VB .NET Outlook 2016 Add-in Subject Line - vb.net

I am writing an add-in for Outlook 2016 using Visual Studio 2015. I added a button to the built-in New Mail tab. When clicked it adds the word "unencrypt" to the end of the subject line and then sends the email.
This works fine as long as the user has tabbed out of the subject line field after entering the subject. But if you type in the subject and then immediately click the button it wipes out the subject line and replaces it with "unencrypt".
However, when I step through in debug it works fine - it keeps the existing text even if I haven't tabbed out of the subject line. I figured there was some sort of delay in updating the Subject property of the mail item, but I manually put in a delay of 20 seconds and it still wiped out the subject line if I wasn't stepping through in debug.
I'm at a loss here. Is there a way to check the subject line textbox itself? or some other way to grab the text even if the user hasn't tabbed out?
Any help would be appreciated!
Private Sub Button1_Click(sender As Object, e As RibbonControlEventArgs) Handles Button1.Click
' Get the Application object
Dim application As Outlook.Application = Globals.ThisAddIn.Application
' Get the active Inspector object and check if is type of MailItem
Dim inspector As Outlook.Inspector = application.ActiveInspector()
Dim mailItem As Outlook.MailItem = TryCast(inspector.CurrentItem, Outlook.MailItem)
If mailItem IsNot Nothing Then
If mailItem.EntryID Is Nothing Then
If Not IsNothing(mailItem.Subject) AndAlso ((mailItem.Subject.Contains(" unencrypt")) OrElse (mailItem.Subject.Contains("unencrypt "))) Then
mailItem.Subject = mailItem.Subject
'ElseIf IsNothing(mailItem.Subject) Then
'System.Threading.Thread.Sleep(20000)
'mailItem.Subject = mailItem.Subject + " unencrypt"
Else
mailItem.Subject = mailItem.Subject + " unencrypt"
End If
If Not IsNothing(mailItem.To) AndAlso mailItem.To.ToString().Trim <> "" Then
mailItem.Send()
Else
MessageBox.Show("We need to know who to send this to. Make sure you enter at least one name.", "Microsoft Outlook", MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1)
End If
End If
End If
End Sub
Edit:
Dmitry's answer got me where I needed, but for anyone else not familiar with the Windows API I added the code below and then simply called the GetSubject function from my original code instead of using the mailItem.Subject property.
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function FindWindowEx(ByVal parentHandle As IntPtr, _
ByVal childAfter As IntPtr, _
ByVal lclassName As String, _
ByVal windowTitle As String) As IntPtr
End Function
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function FindWindow(ByVal lclassName As String, _
ByVal lWindowName As String) As IntPtr
End Function
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function GetWindowText(ByVal hWnd As IntPtr, _
ByVal lpString As StringBuilder, _
ByVal nMaxCount As Integer) As Integer
End Function
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function GetWindowTextLength(ByVal hwnd As IntPtr) As Integer
End Function
Private Function GetSubject(inspector As Outlook.Inspector) As String
Try
Dim inspectorHandle As IntPtr = FindWindow("rctrl_renwnd32", inspector.Caption)
Dim windowLevel2Handle As IntPtr = FindWindowEx(inspectorHandle, IntPtr.Zero, "AfxWndW", "")
Dim windowLevel3Handle As IntPtr = FindWindowEx(windowLevel2Handle, IntPtr.Zero, "AfxWndW", "")
Dim windowLevel4Handle As IntPtr = FindWindowEx(windowLevel3Handle, IntPtr.Zero, "#32770", "")
Dim SubjectHandle As IntPtr = FindWindowEx(windowLevel4Handle, IntPtr.Zero, "Static", "S&ubject")
Dim SubjectTextBoxHandle As IntPtr = FindWindowEx(windowLevel4Handle, SubjectHandle, "RichEdit20WPT", "")
Dim length As Integer = GetWindowTextLength(SubjectTextBoxHandle)
Dim sb As New StringBuilder(length + 1)
GetWindowText(SubjectTextBoxHandle, sb, sb.Capacity)
Return sb.ToString()
Catch
Return ""
End Try
End Function

The important part is that the subject edit box needs to lose focus for the OOM to become aware of the change.
You can use accessibility API or raw Windows API to access the contents of the edit box or you can try to focus some other inspector control, such as the message body editor.

Related

AddFontResource / PrivateFontCollection doesn't make the font immediately available for use in my application

I'm trying to use a PrivateFontCollection for my application, so it can print a document with a specific font. note that i can not "install" the font as the Windows directory is admin protected.
The code I have works, in the sense that provided I close my application, and restart it, when i restart it, it will recognise that the font is there and can be used. But if I click the command button to install the font as a privatefontcollection, and then refresh my PrintDocument, it does not show it using the newly installed font. I have to close the app and open it, and then it does.
Public Shared Function AddFontResource(ByVal lpFileName As String) As Integer
End Function
<DllImport("user32.dll")>
Public Shared Function SendMessage(ByVal hWnd As Integer, ByVal Msg As UInteger, ByVal wParam As Integer, ByVal lParam As Integer) As Integer
End Function
<DllImport("kernel32.dll", SetLastError:=True)>
Shared Function WriteProfileString(ByVal lpszSection As String, ByVal lpszKeyName As String, ByVal lpszString As String) As Integer
End Function
<DllImport("user32.dll", SetLastError:=True)>
Public Shared Function SendMessageTimeout(ByVal hWnd As IntPtr,
ByVal msg As Integer,
ByVal wParam As IntPtr,
ByVal lParam As IntPtr,
ByVal flags As SendMessageTimeoutFlags,
ByVal timeout As Integer,
ByRef result As IntPtr) As IntPtr
End Function
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
Public Shared Function SendNotifyMessage(
ByVal hWnd As IntPtr,
ByVal msg As UInteger,
ByVal wParam As UIntPtr,
ByVal lParam As IntPtr
) As Boolean
End Function
<Flags()>
Public Enum SendMessageTimeoutFlags
SMTO_NORMAL = 0
SMTO_BLOCK = 1
SMTO_ABORTIFHUNG = 2
SMTO_NOTIMEOUTIFNOTHUNG = 8
End Enum
Private Sub RibbonButton1_Click(sender As Object, e As EventArgs) Handles RibbonButton1.Click
Try
If IsFontInstalled("Open Sans ExtraBold") = False Then
Dim Fonts_Source As String = Path.Combine(Application.StartupPath, "Resources\OpenSans-ExtraBold.ttf")
Dim Fonts_Install As String = My.Computer.FileSystem.CombinePath(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "OpenSans-ExtraBold.ttf")
Dim Ret As Integer
Dim Res As Integer
Dim FontPath As String
Const WM_FONTCHANGE As Integer = &H1D
Const HWND_BROADCAST As Integer = &HFFFF
FontPath = Fonts_Install.ToString
Ret = AddFontResource(Fonts_Source.ToString)
Res = SendMessageTimeout(HWND_BROADCAST, WM_FONTCHANGE, IntPtr.Zero, IntPtr.Zero,
SendMessageTimeoutFlags.SMTO_ABORTIFHUNG Or
SendMessageTimeoutFlags.SMTO_NOTIMEOUTIFNOTHUNG,
5000, IntPtr.Zero)
Ret = WriteProfileString("Fonts", Path.GetFileName(FontPath) & " (TrueType)", FontPath.ToString)
End If
Catch ex As Exception
MsgBox("Error: " & ex.Message)
End Try
End Sub
This next subroutine is the one that draws the document. I have a function to check if the font is installed, and if its not then the an alternative font is used.
Dim TitleFont As New Font("Segoe UI Black", Font48Pt, FontStyle.Bold)
If IsFontInstalled("Open Sans ExtraBold") = True Then TitleFont = New Font("Open Sans ExtraBold", Font48Pt)
If Title <> "Everyday" Then
'TITLE TEXT DRAWN
Dim TitleRect As RectangleF = New RectangleF()
TitleRect.Location = New Point(20, 25)
TitleRect.Size = New Size(DrawWidth, CInt(e.Graphics.MeasureString(Title, TitleFont, DrawWidth, CenterAlignment).Height))
e.Graphics.DrawString(Title, TitleFont, ForeColourBrush, TitleRect, CenterAlignment)
End If
the function that checks if the font is installed.
Public Function IsFontInstalled(ByVal FontName As String) As Boolean
Using TestFont As Font = New Font(FontName, 10)
Return CBool(String.Compare(FontName, TestFont.Name, StringComparison.InvariantCultureIgnoreCase) = 0)
End Using
End Function
this function above could be the problem as it returns false. mind you if I close the app and restart it, then this same function will then detect the privatefontcollection and return true, and I can successfully print my document with my font.
I did try adding the install font subroutine, to the very start of my app. then raising a flag if a font was installed and then tried to call the Application.Restart() method, so that while the splash screen was up it could install the font, then immediately restart the app, which of course would then latch onto the installed font, but this method just left the app in a loop of opening and closing.

How to set the Explorer window of a specific file as a child window of TopMost form?

I made some research, but I can't find something really "interesting". I tried my best to find any kind of documentation or questions that are closest to my case as following:
How to find main window title name of application
how to get the window title of a process
How to get the Title Bar Text by its Process Id
getting the name of a process
How do I get list of Process Names running
Check to see if process is running
How To Get Process Owner ID
How to get the title/name of the last active window?
Get Process ID from Window Title
and also
Process.GetProcessesByName Method
The code I am using to open the process window
Private Async Function ParentMethod() As Task
Dim filePath As String = Await Task.Run(
Function()
Return Directory.EnumerateFiles(My.Settings.Cartellasalvataggio, titolo & ".mp3",
SearchOption.AllDirectories).FirstOrDefault()
End Function)
If Not String.IsNullOrEmpty(filePath) Then
LinkLabel1.Text = "File exist already"
LinkLabel1.Visible = True
PictureBox7.Visible = True
Else
MsgBox("it doesn't exist")
End If
End Function
and the helper class
Imports System.IO
Imports System.Runtime.InteropServices
Public Class NativeMethods
<DllImport("shell32.dll", SetLastError:=True)>
Private Shared Function SHOpenFolderAndSelectItems(
pidlFolder As IntPtr, cidl As UInteger,
<[In], MarshalAs(UnmanagedType.LPArray)> apidl As IntPtr(),
dwFlags As UInteger) As Integer
End Function
<DllImport("shell32.dll", SetLastError:=True)>
Private Shared Sub SHParseDisplayName(
<MarshalAs(UnmanagedType.LPWStr)> name As String,
bindingContext As IntPtr, <Out> ByRef pidl As IntPtr,
sfgaoIn As UInteger, <Out> ByRef psfgaoOut As UInteger)
End Sub
Public Shared Sub OpenFolderAndSelectFile(filePath As String)
Dim dirPath As String = Path.GetDirectoryName(filePath)
Dim fileName As String = Path.GetFileName(filePath)
OpenFolderAndSelectFile(dirPath, fileName)
End Sub
Public Shared Sub OpenFolderAndSelectFile(dirPath As String, fileName As String)
Dim nativeFolder As IntPtr
Dim psfgaoOut As UInteger
SHParseDisplayName(dirPath, IntPtr.Zero, nativeFolder, 0, psfgaoOut)
If nativeFolder = IntPtr.Zero Then
' Log error, can't find folder
Return
End If
Dim nativeFile As IntPtr
SHParseDisplayName(Path.Combine(dirPath, fileName),
IntPtr.Zero, nativeFile, 0, psfgaoOut)
Dim fileArray As IntPtr()
If nativeFile = IntPtr.Zero Then
' Open the folder without the file selected if we can't find the file
fileArray = New IntPtr(-1) {}
Else
fileArray = New IntPtr() {nativeFile}
End If
SHOpenFolderAndSelectItems(nativeFolder, CUInt(fileArray.Length), fileArray, 0)
Marshal.FreeCoTaskMem(nativeFolder)
If nativeFile <> IntPtr.Zero Then
Marshal.FreeCoTaskMem(nativeFile)
End If
End Sub
End Class
then calling it with
NativeMethods.OpenFolderAndSelectFile(filepath,filename & "extension"))
Since I am opening the process this way and NOT with Process class, almost all of them are not suitable to be considered for my case as many of them refer to notepad, while I think the explorer window title and ID changes for every file ( obviously), while "notepad" process, stay "notepad".
I also tried BringToFront, but this latter moves a control in front of other controls, but in this case Explorer is not a control, right?
The least I want to do is to
Get a list of active windows & their process names
as It will waste memory and time usage for no reason as I will need to "filter" process to find my process.
Hope we can find a solution to this, Thanks in advance.
Mattia
This is the solution to it using FindWindowW e SetWindowPos Api.
It is showing Explorer folder on top of top most form.
<DllImport("user32.dll", EntryPoint:="FindWindowW")>
Public Shared Function FindWindowW(<MarshalAs(UnmanagedType.LPTStr)> ByVal lpClassName As String, <MarshalAs(UnmanagedType.LPTStr)> ByVal lpWindowName As String) As IntPtr
End Function
<DllImport("user32.dll")>
Shared Function SetWindowPos(ByVal hWnd As IntPtr, ByVal hWndInsertAfter As IntPtr, ByVal X As Integer, ByVal Y As Integer, ByVal cx As Integer, ByVal cy As Integer, ByVal uFlags As UInteger) As Boolean
End Function
Shared ReadOnly HWND_TOPMOST As IntPtr = New IntPtr(-1)
Const SWP_NOSIZE As UInt32 = &H1
Const SWP_NOMOVE As UInt32 = &H2
Const SWP_SHOWWINDOW As UInt32 = &H40
Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
Dim inptr = FindWindowW("CabinetWClass", Nothing)
SetWindowPos(inptr, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE Or SWP_NOSIZE Or SWP_SHOWWINDOW)
End Sub

Open Folder Path if not already open, if open show window

This seems like a simple task but I cannot seem to produce the results looking for.
currently I have this code
Dim folderpath As String
Private Sub Button4_Click(sender As Object, e As EventArgs) Handles Button4.Click
folderpath = "C:\epds\WIP"
Process.Start("explorer.exe", folderpath)
End Sub
That's fine and it opens my folder path as indicated, however, if the instance of that folder is already open in the explorer how do I just make that window current instead of opening a new window explorer?
EDIT: This seemed to do the trick, thanks for pointing me in the right direction #Okuma.Scott
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function FindWindow( _
ByVal lpClassName As String, _
ByVal lpWindowName As String) As IntPtr
End Function
<DllImport("user32.dll", EntryPoint:="FindWindow", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function FindWindowByClass( _
ByVal lpClassName As String, _
ByVal zero As IntPtr) As IntPtr
End Function
<DllImport("user32.dll", EntryPoint:="FindWindow", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function FindWindowByCaption( _
ByVal zero As IntPtr, _
ByVal lpWindowName As String) As IntPtr
End Function
Dim folderpath As String
Private Sub Button4_Click(sender As Object, e As EventArgs) Handles Button4.Click
folderpath = "C:\epds\WIP"
'Process.Start("explorer.exe", folderpath)
Dim nWnd As IntPtr
Dim ceroIntPtr As New IntPtr(0)
Dim Wnd_name As String
Wnd_name = "WIP"
nWnd = FindWindow(Nothing, Wnd_name)
'show the info
If nWnd.Equals(ceroIntPtr) Then
Process.Start("explorer.exe", folderpath)
Else
AppActivate(Wnd_name)
SendKeys.SendWait("~")
End If
End Sub
I was trying to solve this same issue and discovered that it seems to work by just calling Process.Start with the desired path:
Process.Start("C:\Temp")
If the folder is already open in an Explorer window it opens the existing window otherwise it opens a new window.
you need to import Imports System.Runtime.InteropServices
then you can use the function Findwindow
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function FindWindow(ByVal lpClassName As String, ByVal lpWindowName As String) As IntPtr
End Function
Then make 2 dims 1. folderpath and 2 is foldername
Then in your click event use "System.IO.Path.GetFileName(folderpath)" to get the name of the window you are looking for." for you WIP"
Then check with a if statement if FindWindow(vbNullString, foldername) = 0 "not open"
The vbNullString Represents a zero-length string for print and display functions, and for calling external procedures."msdn"
so if findwindow is 0 open the folder and else focus the folder
Dim folderpath As String
Dim foldername As String
Private Sub Button4_Click(sender As Object, e As EventArgs) Handles Button4.Click
folderpath = "C:\epds\WIP"
foldername = System.IO.Path.GetFileName(folderpath)
If FindWindow(vbNullString, foldername) = 0 Then
Process.Start("explorer.exe", folderpath)
Else
AppActivate(foldername)
SendKeys.SendWait("~")
End If
End Sub
This works and will not open multiple windows:
Process.Start(new ProcessStartInfo() { FileName = "C:\\", UseShellExecute = true });
The only downside is that it does not bring the opened folder to the foreground (which depending on your use case may or may not be a bad thing!

Passing value to excel inputbox from VB.NET

I am trying to automate data population on some excel sheets that have some macros. Now the excel is protected and I cannot get the secret key. Now I am able to run the macros but when I try to pass arguments I get arguments mismatch.
If I just run the macro with the name, I get an inputbox which takes an extra argument as input and auto generates some of the values for the columns. I have to manually enter this value into the inputbox as of now. Is there any way that I could automate that process, i.e capture the inputbox thrown by the macro in the vb.net script and enter the values from there? i.e., I would like to run the macro and after I get the popup asking me to enter some value, use the vb.net code to enter the value to that popup.
Here is what I have till now
Public Class Form1
Dim excelApp As New Excel.Application
Dim excelWorkbook As Excel.Workbook
Dim excelWorkSheet As Excel.Worksheet
Private Sub Button4_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button4.Click
excelWorkbook = excelApp.Workbooks.Open("D:/excelSheets/plan_management_data_templates_network.xls")
excelApp.Visible = True
excelWorkSheet = excelWorkbook.Sheets("Networks")
With excelWorkSheet
.Range("B7").Value = "AR"
End With
excelApp.Run("createNetworks")
// now here I would like to enter the value into the createNetworks Popup box
excelApp.Quit()
releaseObject(excelApp)
releaseObject(excelWorkbook)
End Sub
Macro definition
createNetworks()
//does so basic comparisons on existing populated fields
//if true prompts an inputbox and waits for user input.
This stall my vb.net script too from moving to the next line.
Like you and me, we both have names, similarly windows have handles(hWnd), Class etc. Once you know what that hWnd is, it is easier to interact with that window.
This is the screenshot of the InputBox
Logic:
Find the Handle of the InputBox using FindWindow and the caption of the Input Box which is Create Network IDs
Once that is found, find the handle of the Edit Box in that window using FindWindowEx
Once the handle of the Edit Box is found, simply use SendMessage to write to it.
In the below example we would be writing It is possible to Interact with InputBox from VB.Net to the Excel Inputbox.
Code:
Create a Form and add a button to it.
Paste this code
Imports System.Runtime.InteropServices
Imports System.Text
Public Class Form1
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As Integer
Private Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" _
(ByVal hWnd1 As Integer, ByVal hWnd2 As Integer, ByVal lpsz1 As String, _
ByVal lpsz2 As String) As Integer
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" _
(ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Integer, _
ByVal lParam As String) As Integer
Const WM_SETTEXT = &HC
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim Ret As Integer, ChildRet As Integer
'~~> String we want to write to Input Box
Dim sMsg As String = "It is possible to Interact with InputBox from VB.Net"
'~~> Get the handle of the "Input Box" Window
Ret = FindWindow(vbNullString, "Create Network IDs")
If Ret <> 0 Then
'MessageBox.Show("Input Box Window Found")
'~~> Get the handle of the Text Area "Window"
ChildRet = FindWindowEx(Ret, 0, "EDTBX", vbNullString)
'~~> Check if we found it or not
If ChildRet <> 0 Then
'MessageBox.Show("Text Area Window Found")
SendMess(sMsg, ChildRet)
End If
End If
End Sub
Sub SendMess(ByVal Message As String, ByVal hwnd As Long)
Call SendMessage(hwnd, WM_SETTEXT, False, Message)
End Sub
End Class
ScreenShot
When you run the code this is what you get
EDIT (Based on further request of automating the OK/Cancel in Chat)
AUTOMATING THE OK/CANCEL BUTTONS OF INPUTBOX
Ok here is an interesting fact.
You can call the InputBox function two ways in Excel
Sub Sample1()
Dim Ret
Ret = Application.InputBox("Called Via Application.InputBox", "Sample Title")
End Sub
and
Sub Sample2()
Dim Ret
Ret = InputBox("Called Via InputBox", "Sample Title")
End Sub
In your case the first way is used and unfortunately, The OK and CANCEL buttons do not have a handle so unfortunately, you will have to use SendKeys (Ouch!!!) to interact with it. Had you Inbutbox been generated via the second method then we could have automated the OK and CANCEL buttons easily :)
Additional Info:
Tested on Visual Studio 2010 Ultimate (64 bit) / Excel 2010 (32 bit)
Inspired by your question, I actually wrote a blog Article on how to interact with the OK button on InputBox.
Currently, I employ a method where I run a thread before the macro is called by the script. The thread checks if the inputbox has been called. If it is, it picks up the value from the location and using sendkeys, submits the box.
This is a rudimentary solution but I was hoping for a more elegant solution to this problem.
My solution Code:
Public Class Form1
Dim excelApp As New Excel.Application
Dim excelWorkbook As Excel.Workbook
Dim excelWorkSheet As Excel.Worksheet
Private Sub Button4_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button4.Click
excelWorkbook = excelApp.Workbooks.Open("D:/excelSheets/some_excel.xls")
excelApp.Visible = True
excelWorkSheet = excelWorkbook.Sheets("SheetName")
With excelWorkSheet
.Range("B7").Value = "Value"
End With
Dim trd = New Thread(Sub() Me.SendInputs("ValueForInputBox"))
trd.IsBackground = True
trd.Start()
excelApp.Run("macroName")
trd.Join()
releaseObject(trd)
excelApp.Quit()
releaseObject(excelApp)
releaseObject(excelWorkbook)
End Sub
Private Sub releaseObject(ByVal obj As Object)
Try
System.Runtime.InteropServices.Marshal.ReleaseComObject(obj)
obj = Nothing
Catch ex As Exception
obj = Nothing
Finally
GC.Collect()
End Try
End Sub
Private Sub SendInputs(ByVal noOfIds As String)
Thread.Sleep(100)
SendKeys.SendWait(noOfIds)
SendKeys.SendWait("{ENTER}")
SendKeys.SendWait("{ENTER}")
End Sub

TBPF_INDETERMINATE status has no effect in .NET 2.0 on Windows 7 taskbar

I have a very weird problem. I have a VB.NET 2.0 application that takes advantage of Windows 7 taskbar button progress features, i.e. displaying certain progress and application state in the Win7 taskbar button.
Everything works just fine - I can set and update progress, I can set the button to paused or ewrror state, I can set it to No progress. Everything works, except MARQUE (Indeterminate) mode. This is a total mistery, whenever I set state to TBPF_INDETERMINATE (value of 0x1), it simply changes back to NOPROGRESS type, i.e. it removes all progress inidcation from the taskbar button and sets it back to its default state - no animated marque is ever displayed!
I have read documentation on MSDN - http://msdn.microsoft.com/en-us/library/dd391697(v=vs.85).aspx ; tried various combinations like setting progress to 0 and then calling set state to indeterminate; or like setting it to normal first and then to indeterminate - nothing works. It's a total mistery - and there is no clue in the documentation as to why it is failing...
Here's the code:
The API implementation:
<StructLayout(LayoutKind.Sequential)> _
Public Structure RECT
Public left As Integer
Public top As Integer
Public right As Integer
Public bottom As Integer
Public Sub New(left As Integer, top As Integer, right As Integer, bottom As Integer)
Me.left = left
Me.top = top
Me.right = right
Me.bottom = bottom
End Sub
End Structure
Public Enum TBPFLAG
TBPF_NOPROGRESS = 0
TBPF_INDETERMINATE = &H1
TBPF_NORMAL = &H2
TBPF_ERROR = &H4
TBPF_PAUSED = &H8
End Enum
Public Enum TBATFLAG
TBATF_USEMDITHUMBNAIL = &H1
TBATF_USEMDILIVEPREVIEW = &H2
End Enum
Public Enum THBMASK
THB_BITMAP = &H1
THB_ICON = &H2
THB_TOOLTIP = &H4
THB_FLAGS = &H8
End Enum
Public Enum THBFLAGS
THBF_ENABLED = 0
THBF_DISABLED = &H1
THBF_DISMISSONCLICK = &H2
THBF_NOBACKGROUND = &H4
THBF_HIDDEN = &H8
End Enum
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)> _
Public Structure THUMBBUTTON
<MarshalAs(UnmanagedType.U4)> _
Public dwMask As THBMASK
Public iId As UInteger
Public iBitmap As UInteger
Public hIcon As IntPtr
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=260)> _
Public szTip As String
<MarshalAs(UnmanagedType.U4)> _
Public dwFlags As THBFLAGS
End Structure
<ComImportAttribute()> _
<GuidAttribute("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")> _
<InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface ITaskbarList3
' ITaskbarList
<PreserveSig()> _
Sub HrInit()
<PreserveSig()> _
Sub AddTab(hwnd As IntPtr)
<PreserveSig()> _
Sub DeleteTab(hwnd As IntPtr)
<PreserveSig()> _
Sub ActivateTab(hwnd As IntPtr)
<PreserveSig()> _
Sub SetActiveAlt(hwnd As IntPtr)
' ITaskbarList2
<PreserveSig()> _
Sub MarkFullscreenWindow(hwnd As IntPtr, <MarshalAs(UnmanagedType.Bool)> fFullscreen As Boolean)
' ITaskbarList3
Sub SetProgressValue(<[In]()> ByVal hwnd As IntPtr, <[In]()> ByVal ullCompleted As UInt64, <[In]()> ByVal ullTotal As UInt64) 'hwnd As IntPtr, ullCompleted As UInt64, ullTotal As UInt64)
Sub SetProgressState(<[In]()> ByVal hwnd As IntPtr, <[In]()> ByVal tbpFlags As TBPFLAG) 'hwnd As IntPtr, tbpFlags As TBPFLAG) 'As Integer
Sub RegisterTab(hwndTab As IntPtr, hwndMDI As IntPtr)
Sub UnregisterTab(hwndTab As IntPtr)
Sub SetTabOrder(hwndTab As IntPtr, hwndInsertBefore As IntPtr)
Sub SetTabActive(hwndTab As IntPtr, hwndMDI As IntPtr, tbatFlags As TBATFLAG)
Sub ThumbBarAddButtons(hwnd As IntPtr, cButtons As UInteger, <MarshalAs(UnmanagedType.LPArray)> pButtons As THUMBBUTTON())
Sub ThumbBarUpdateButtons(hwnd As IntPtr, cButtons As UInteger, <MarshalAs(UnmanagedType.LPArray)> pButtons As THUMBBUTTON())
Sub ThumbBarSetImageList(hwnd As IntPtr, himl As IntPtr)
Sub SetOverlayIcon(hwnd As IntPtr, hIcon As IntPtr, <MarshalAs(UnmanagedType.LPWStr)> pszDescription As String)
Sub SetThumbnailTooltip(hwnd As IntPtr, <MarshalAs(UnmanagedType.LPWStr)> pszTip As String)
'[MarshalAs(UnmanagedType.LPStruct)]
Sub SetThumbnailClip(hwnd As IntPtr, ByRef prcClip As RECT)
End Interface
<GuidAttribute("56FDF344-FD6D-11d0-958A-006097C9A090")> _
<ClassInterfaceAttribute(ClassInterfaceType.None)> _
<ComImportAttribute()> _
Public Class CTaskbarList
End Class
And here are the actual procedures that use the code:
Friend Sub SetWindows7Progress(ByVal aValue As Integer)
If Not IsWin7orLater Then Exit Sub
If w7tb Is Nothing Then
w7tb = CType(New CTaskbarList, ITaskbarList3)
End If
CType(w7tb, ITaskbarList3).SetProgressValue(My.Forms.Form1.Handle, Math.Min(Math.Max(1, aValue), 1000), 1000)
End Sub
Friend Sub ResetWindows7Progress()
If Not IsWin7orLater Then Exit Sub
If w7tb Is Nothing Then
w7tb = CType(New CTaskbarList, ITaskbarList3)
End If
CType(w7tb, ITaskbarList3).SetProgressState(My.Forms.Form1.Handle, TBPFLAG.TBPF_NOPROGRESS)
End Sub
Friend Sub SetWindows7ProgressMon()
If Not IsWin7orLater Then Exit Sub
If w7tb Is Nothing Then
w7tb = CType(New CTaskbarList, ITaskbarList3)
End If
CType(w7tb, ITaskbarList3).SetProgressState(My.Forms.Form1.Handle, TBPF_INDETERMINATE)
End Sub
I even tried getting the HRESULT code from SetProgressState and checking to make sure no exception is thrown to no avail: SetProgressState always returns 0 (everything is fine); and no exceptions are thrown!
Any help in resolving the matter would be greatly appreciated! I just can't believe that everything works except the MARQUE/INDETERMINATE state!
Thanks.
First, it's very strange that your worker functions are accessing your form's instance using this code:
My.Forms.Form1.Handle
That implies that those functions are not defined in the same class as your form (because if they were, the compiler would prompt you to use Me, instead). And if that's the case, you really should be passing the handle to the form into the function as a parameter.
(The reason for this is so that your functions are reusable. If you hardcode a reference to a particular form, what happens when you rename that form, or display two instances of it on the screen at a time, or just want to show a progress indicator in the taskbar for a different form? Things break. Passing the form instance as a parameter is a much cleaner, more reusable approach.)
Second, there seems like an unnecessary amount of casting going on. Why not just declare the w7tb variable as type ITaskbarList3 in the first place, rather than casting back and forth between that and CTaskbarList?
Third, I'm not sure if this is a typo or the actual problem, but your SetWindows7ProgressMon function does not actually reference the correct value for TBPF_INDETERMINATE. You use an unqualified reference to that identifier, when it's actually defined in the TBPFLAG enumeration.
So, considering all of the above, I would rewrite the second block of code that you posted as follows:
Private w7tb As ITaskbarList3
Friend Sub SetWindows7Progress(ByVal frm As Form, ByVal aValue As Integer)
If (Not IsWin7orLater()) OrElse (frm Is Nothing) Then
Exit Sub
End If
If w7tb Is Nothing Then
w7tb = CType(New CTaskbarList, ITaskbarList3)
End If
w7tb.SetProgressValue(frm.Handle, Math.Min(Math.Max(1, aValue), 1000), 1000)
End Sub
Friend Sub ResetWindows7Progress(ByVal frm As Form)
If (Not IsWin7orLater()) OrElse (frm Is Nothing) Then
Exit Sub
End If
If w7tb Is Nothing Then
w7tb = CType(New CTaskbarList, ITaskbarList3)
End If
w7tb.SetProgressState(frm.Handle, TBPFLAG.TBPF_NOPROGRESS)
End Sub
Friend Sub SetWindows7ProgressMon(ByVal frm As Form)
If (Not IsWin7orLater()) OrElse (frm Is Nothing) Then
Exit Sub
End If
If w7tb Is Nothing Then
w7tb = CType(New CTaskbarList, ITaskbarList3)
End If
w7tb.SetProgressState(frm.Handle, TBPFLAG.TBPF_INDETERMINATE)
End Sub
This is tested to work perfectly on Windows 7 32-bit. Note that you can call the functions from code inside of your form class by simply specifying Me for the frm parameter.