Simulating Taskbar tray

Hi!

We have about eight servers in a terminalserver-farm (W2k3 R2 x64) used to access various applications installed on them.
Now we need to access some websites that need smartcard authentication. The program that is needed for that is called "siecacst.exe" (Siemens Smartcard Program) and manages the certificates on the card.

So here's the problem: The above mentioned program puts itself to the notification tray on the right side of the taskbar. This is fine when the explorer.exe process is running. But unfortunately we did not use the explorer.exe as shell but another self-developed "Launcher" which replaces the default shell. So there is no more tray where the program can go into. The program quits with an error message saying that it cannot put itself to the tray, grrrr...

As the developer of the smartcard-tool is not willing to modify it's program, I need another way to tell it that it was successfully minimized to the tray.

So heres the question: Is it possible, maybe with capturing windows messages (overriding hwnd, sendmessage...), to simulate a successful placement in tray, even if there's no real tray (as there's no explorer process running)?

The Launcher that is used as a shell-replacement is written by me (or myself? (sorry, I'm german :))), so I could just implement some sort of a fake-tray...

The workaround that is now in test is to start the explorer but disable anything via group policy and hiding the taskbar using api. But I don't like this method as it loads tons of other not needed processes...

btw. Language preferred is vb.net, but c# is also fine.

Thanks a lot!
LVL 1
janwrageAsked:
Who is Participating?

[Product update] Infrastructure Analysis Tool is now available with Business Accounts.Learn More

x
I wear a lot of hats...

"The solutions and answers provided on Experts Exchange have been extremely helpful to me over the last few years. I wear a lot of hats - Developer, Database Administrator, Help Desk, etc., so I know a lot of things but not a lot about one thing. Experts Exchange gives me answers from people who do know a lot about one thing, in a easy to use platform." -Todd S.

nffvrxqgrcfqvvcCommented:
While I have never tried this in theory I think you can actually make this work.... The idea is to use RegisterClass the class name you want to register would be Shell_TrayWnd. You will also need to specify a callback address (WndProc) this is where the magic will happen later. After you register the class call CreateWindowEx() it's important that in the (WndProc) you handle WM_NCCREATE, WM_CREATE messages otherwise you will get a NULL handle when CreateWindowEx() returns.
The idea here is that if (explorer) isn't active the messages will be forwarded to your window which you should catch WM_COPYDATA messages. Copy lParam pointer to COPYDATASTRUCT. The lpData member of this structure points to a similar structure of NOTIFYICONDATA but it's not exactly the same I do know that the first 4 bytes is hWnd and the the next 4 points to the Id member this is actually the message used to identify how the icon should be placed into tray... NIM_ADD,NIM_MODIFY,NIM_DELETE.
Depending on how the applicationw was written all you may need to do is mimic the window and forward the messages if this isn't enough you might try to catch the NIM_ADD and return either 0 or 1 I can't be sure which one would inform the application it was added.


nffvrxqgrcfqvvcCommented:
Okay I just confirmed the theory does work so if the smart card application is using Shell_NotifyIcon API and if the checksum is the return value you must process NIM_ADD message and return a value of 1 in the WndProc. This tricks the API to return TRUE which may bypass the check in the application if this is how it works.
nffvrxqgrcfqvvcCommented:
ShellData is what I could make of the return data only tested on XP32 bit so it's possible the structure could be different on Vista and later. If you kill explorer and run this it should detect when an application adds a system tray button. You can get the hWnd, hIcon, Tooltip text of this application but as mentioned before the ShellData layout might need to be altered for other versions of windows and probrably would need addtional padding on 64 bit....

Imports System.Runtime.InteropServices


Public Class ShellTrayWnd
    'egl1044
    Private Const CS_OWNDC = &H20

    Private Enum WinShellTrayMsg
        NIM_ADD = &H0
        NIM_MODIFY = &H1
        NIM_DELETE = &H2
    End Enum
    Private Enum WinDefaultMsg
        WM_NCCREATE = &H81
        WM_CREATE = &H1
        WM_COPYDATA = &H4A
    End Enum

    <StructLayout(LayoutKind.Sequential)> _
    Private Structure COPYDATASTRUCT
        Dim dwData As IntPtr
        Dim cbData As IntPtr
        Dim lpData As IntPtr
    End Structure

    <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)> _
    Private Structure WNDCLASSEX
        Dim cbSize As Integer
        Dim style As Integer
        Dim lpfnWndProc As WindowProcDelegate 'WNDPROC   
        Dim cbClsExtra As Integer
        Dim cbWndExtra As Integer
        Dim hInstance As IntPtr 'HINSTANCE
        Dim hIcon As IntPtr 'HICON
        Dim hCursor As IntPtr 'HCURSOR
        Dim hbrBackground As IntPtr 'HBRUSH
        Dim lpszMenuName As IntPtr
        Dim lpszClassName As IntPtr
        Dim hIconSm As IntPtr 'HICON
    End Structure
  
    <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)> _
    Private Structure ShellData
        Dim Unknown1 As Integer
        Dim Message As Integer 'WinShellTrayMsg
        Dim Unknown2 As Integer
        Dim hWnd As IntPtr
        Dim Unknown3 As Integer
        Dim uFlags As Integer
        Dim Unknown4 As Integer
        Dim hIcon As IntPtr
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=64)> Dim szTip As String
    End Structure

    <DllImport("user32.dll", CharSet:=CharSet.Unicode, SetLastError:=True)> _
    Private Shared Function CreateWindowEx(ByVal dwExStyle As Integer, ByVal lpClassName As IntPtr, ByVal lpWindowName As IntPtr, ByVal dwStyle As Integer, ByVal x As Integer, ByVal y As Integer, ByVal nWidth As Integer, ByVal nHeight As Integer, ByVal hWndParent As IntPtr, ByVal hMenu As IntPtr, ByVal hInstance As IntPtr, ByVal lParam As IntPtr) As IntPtr
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Unicode, SetLastError:=True)> _
    Private Shared Function CallWindowProc(ByVal lpPrevWndFunc As IntPtr, ByVal hWnd As Integer, ByVal Msg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As IntPtr
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Unicode, SetLastError:=True)> _
    Private Shared Function RegisterClassEx(ByRef lpwcx As WNDCLASSEX) As IntPtr
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Unicode, SetLastError:=True)> _
    Private Shared Function UnregisterClass(ByVal lpClassName As IntPtr, ByVal hInstance As IntPtr) As Integer
    End Function

    <DllImport("kernel32.dll", CharSet:=CharSet.Unicode, SetLastError:=True)> _
    Private Shared Function GetModuleHandle(ByVal lpModuleName As IntPtr) As IntPtr
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Unicode, SetLastError:=True)> _
    Private Shared Function DestroyWindow(ByVal hWnd As IntPtr) As Integer
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Unicode, SetLastError:=True)> _
    Private Shared Function IsWindowUnicode(ByVal hWnd As IntPtr) As Integer
    End Function

    Private Shared winEx As WNDCLASSEX
    Private Shared hAtom As IntPtr = IntPtr.Zero
    Private Shared hProc As IntPtr = IntPtr.Zero
    Private Shared hWin As IntPtr = IntPtr.Zero

    Private Delegate Function WindowProcDelegate(ByVal hWnd As Integer, ByVal uMsg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As IntPtr
    Private Shared Function WindowProc(ByVal hWnd As Integer, ByVal uMsg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As IntPtr

        Dim cds As COPYDATASTRUCT = Nothing
        Dim sds As ShellData = Nothing

        Select Case uMsg
            Case WinDefaultMsg.WM_NCCREATE
                Console.WriteLine("WM_NCCREATE")
                Return New IntPtr(1)
            Case WinDefaultMsg.WM_CREATE
                Console.WriteLine("WM_CREATE")
                Return New IntPtr(0)
            Case WinDefaultMsg.WM_COPYDATA
                cds = Marshal.PtrToStructure(New IntPtr(lParam), cds.GetType)
                sds = Marshal.PtrToStructure(cds.lpData, sds.GetType)
                Select Case sds.Message
                    Case WinShellTrayMsg.NIM_ADD
                        Console.WriteLine("NIM_ADD:     hWnd={0}    hIcon={1}   Tip={2}", sds.hWnd, sds.hIcon, sds.szTip)
                        Return 1
                    Case WinShellTrayMsg.NIM_DELETE
                        Console.WriteLine("NIM_DELETE:  hWnd={0}    hIcon={1}   Tip={2}", sds.hWnd, sds.hIcon, sds.szTip)
                        Return 1
                End Select
        End Select
        Return CallWindowProc(hProc, hWnd, uMsg, wParam, lParam)
    End Function

    Public Shared Sub BeginMimic()

        ' WNDCLASSEX
        winEx.cbSize = Marshal.SizeOf(winEx.GetType)
        winEx.style = CS_OWNDC
        winEx.lpfnWndProc = AddressOf WindowProc
        winEx.cbClsExtra = 0
        winEx.cbWndExtra = 4
        winEx.hInstance = GetModuleHandle(IntPtr.Zero)
        winEx.hIcon = IntPtr.Zero
        winEx.hCursor = IntPtr.Zero
        winEx.hbrBackground = IntPtr.Zero
        winEx.lpszMenuName = IntPtr.Zero
        winEx.lpszClassName = Marshal.StringToHGlobalUni("Shell_TrayWnd")
        winEx.hIconSm = IntPtr.Zero

        ' Register the class information.
        hAtom = RegisterClassEx(winEx)

        If hAtom.ToInt32 = 0 Then
            Console.WriteLine(Marshal.GetLastWin32Error)
            Return
        End If

        ' Create the window using the classname.
        hWin = CreateWindowEx(0, hAtom, IntPtr.Zero, 0, 0, 0, 0, 0, _
                                  IntPtr.Zero, IntPtr.Zero, winEx.hInstance, IntPtr.Zero)

        If hWin.ToInt32 = 0 Then
            EndMimic()
            Console.WriteLine(Marshal.GetLastWin32Error)
            Return
        End If

        ' Check if the window is UNICODE.
        If IsWindowUnicode(hWin) <> 0 Then
            Console.WriteLine("UNICODE Shell_TrayWnd OK.")
            Console.WriteLine("Processing Shell_TrayWnd messages OK.")
        End If

    End Sub

    Public Shared Sub EndMimic()
        ' Cleanup
        If hWin.ToInt32 <> 0 Then
            Console.WriteLine(DestroyWindow(hWin))
            hWin = IntPtr.Zero
        End If
        If hAtom.ToInt32 <> 0 Then
            Console.WriteLine(UnregisterClass(hAtom, winEx.hInstance))
            hAtom = IntPtr.Zero
        End If
        If winEx.lpszClassName.ToInt32 <> 0 Then
            Marshal.ZeroFreeGlobalAllocUnicode(winEx.lpszClassName)
        End If
        winEx = Nothing
    End Sub

End Class

Open in new window

Experts Exchange Solution brought to you by

Your issues matter to us.

Facing a tech roadblock? Get the help and guidance you need from experienced professionals who care. Ask your question anytime, anywhere, with no hassle.

Start your 7-day free trial
CompTIA Network+

Prepare for the CompTIA Network+ exam by learning how to troubleshoot, configure, and manage both wired and wireless networks.

janwrageAuthor Commented:
Hi egl1044!
Thanks for your quick reply, will try ur code tomorrow at work.

I think I need to call BeginMimic, right? This code is heavy stuff as I don't understand much, hehe...would be great if it's working!

I will test it on x64 and post the result. If it fails, could you modify it to fit on x64? Would give you 500Points again.

Thanks again!
Regards
Jan
nffvrxqgrcfqvvcCommented:
Yes BeginMimic and to cleanup use EndMimic. These should only be called once if using a form (Form_Load) (Form_Closing) for example.
The structure should be modified to the following so that it's compatible on both x32 and x64. I made a mistake using IntPtr while this works fine on x32 on x64 IntPtr is 8 bytes so the structure is 8 bytes larger than it should be as it's fixed so just use Integer.
BTW: You don't really need the entire structure that is just some extra info like the hWnd and hIcon and Tooltip text.
You can use either of the following:

<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)> _
    Private Structure ShellData
        Dim Unknown1 As Integer
        Dim Message As Integer 'WinShellTrayMsg
        Dim Unknown2 As Integer
        Dim hWnd As Integer
        Dim Unknown3 As Integer
        Dim uFlags As Integer
        Dim Unknown4 As Integer
        Dim hIcon As Integer
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=64)> Dim szTip As String
    End Structure



    <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)> _
    Private Structure ShellDataSimple
        Dim Unknown1 As Integer
        Dim Message As Integer 'WinShellTrayMsg
    End Structure

Open in new window

janwrageAuthor Commented:
Wow, it's working!
Here's the output:

WM_NCCREATE
WM_CREATE
UNICODE Shell_TrayWnd OK.
Processing Shell_TrayWnd messages OK.
NIM_ADD: 131192, 589975, CardOS API V5.0 (Build 07)
hWin <> 0, 1
hAtom <> 0, 1

Next question: Is this method really stable? What happens if EndMimic could not run, cause of application crash or something. And what if explorer.exe is launched?
janwrageAuthor Commented:
Absolutely fantastic. Works out of the box.
Not easy to understand but that's due to my little knowledge :)
nffvrxqgrcfqvvcCommented:
EndMimic is basic cleanup to unregister the class and destroy the created window. If the application crashes windows will automatically unregister the class and windows. It's not required but for debugging using windows forms and in the designer it's best to perform the cleanup. It can also be used as an ON/OFF switch type of routine.
How stable is the method?
It's perfectly stable for tricking the application it was added to the system tray. You should still keep an eye out to make sure everything runs smooth.
It appears that explorer has the highest priority and therfore when explorer is running all messages are pumped normally to the explorer Shell_TrayWnd instead of the application.
The basic idea here is that applications use Shell_NotifyIcon to register the icon button with windows. This API sends a message to the Shell_TrayWnd in form of WM_COPYDATA with the information. When explorer isn't running these messages either timeout or fail. This makes Shell_NotifyIcon return FALSE.

The smart card application appears to doing this as its checksum.
If Shell_NotifyIcon(NIM_ADD,NOTIFYICONDATA) = TRUE Then
Success
Else
Couldn't add to the system tray (abort application)
End If
What the code does is simply create the Shell_TrayWnd to catch the normal messages that would be processed if explorer was running and handles them to make the API return TRUE as if the message was processed normally bypassing the application level check.
It's more than this solution.Get answers and train to solve all your tech problems - anytime, anywhere.Try it for free Edge Out The Competitionfor your dream job with proven skills and certifications.Get started today Stand Outas the employee with proven skills.Start learning today for free Move Your Career Forwardwith certification training in the latest technologies.Start your trial today
.NET Programming

From novice to tech pro — start learning today.