Win32 Tips & Tricks - Part 2


12 minute read  • 

win32

Continuing on from the last part, here are more Win32 Tips and Tricks for you to browse.

XP Themes

Common-control manifests (which enable the XP themes) can be added to an executable in one of two ways:

Firstly, the manifest can be saved inside a file in the same directory as your executable. It must also have the same filename, with the text ‘.manifest’ appended to the end:

AppName.exe
AppName.exe.manifest

The manifest is just a text file (in XML) and should look like this:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
    <assemblyIdentity    
        version="1.0.0.0"    
        processorArchitecture="X86"    
        name="CompanyName.ProductName.AppName"    
        type="win32"
    />
    <description>Program Description</description>
    <dependency>    
      <dependentAssembly>        
        <assemblyIdentity 
            type="win32" 
            name="Microsoft.Windows.Common-Controls"            
            version="6.0.0.0"            
            processorArchitecture="X86"            
            publicKeyToken="6595b64144ccf1df"            
            language="*"        
        />    
      </dependentAssembly>
    </dependency>
  </assembly>
</xml>

You should change the <assemblyIdentity name=“MyProg”> value to the same name as your executable.

The second method of adding the manifest is to embed it directly into the executable as a resource:

  1. Add a Custom Resource to your application with resource-type name “24”
  2. Change the resource-id to “1”
  3. Embed the manifest directly into the resource as binary data.

Safe Subclassing

Use the following function to safely sublass a window when you don’t know if it is a Unicode/Ansi window or not:

WNDPROC SafeSubclassWindow(HWND hwnd, WNDPROC NewProc)
{
    if(IsWindowUnicode(hwnd))
        return (WNDPROC)SetWindowLongPtrW(hwnd, GWL_WNDPROC, (LONG_PTR)NewProc);
    else
        return (WNDPROC)SetWindowLongPtrA(hwnd, GWL_WNDPROC, (LONG_PTR)NewProc);
}

Your sublass procedure must now call the old window-procedure by making use of the following function, which calls the Unicode or Ansi version of CallWindowProc as appropriate:

LRESULT SafeCallWndProc(WNDPROC OldProc, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    if(IsWindowUnicode(hwnd))
        return CallWindowProcW(OldProc, hwnd, msg, wParam, lParam);
    else
        return CallWindowProcA(OldProc, hwnd, msg, wParam, lParam);
}

Shell Dialog Fonts

Under Windows XP and 2000 a new shell fontname was introduced - “MS Shell Dlg”. This is not a real font - instead it is a registry value which maps to whatever the current user-interface font is (usually Tahoma/Segoi UI etc). To use this new font for your dialogs, you must edit your resource.rc file manually and change the name to “MS Shell Dlg”. You must also add the DS_SHELLFONT style to the dialog.

Note that there is a trick to getting this to work correctly. The dialog-template resource must a DIALOGEX resource type rather than the older DIALOG. You can add an extended dialog-style to the resource (such as WS_EX_CONTROLPARENT) to cause this change, because otherwise Visual Studio will think that it is an older DIALOG and will erase your work once you make any changes to the template.

Lastly if your dialog is going to be part of a property-sheet, then all panes must be DIALOGEX’s otherwise the font will not work.

SHBrowseForFolder

It is possible to force SHBrowseForFolder start at a specific directory by using the callback function facility:

BOOL BrowseForPath(HWND hwndParent, TCHAR *szPath)
{	
    BROWSEINFO shbi = { 0 };
    ITEMIDLIST *pidl;
    BOOL success = FALSE;
    IMalloc *pMalloc;
    
    shbi.hwndOwner = hwndParent;
    shbi.ulFlags = BIF_NONEWFOLDERBUTTON | BIF_RETURNONLYFSDIRS;
    shbi.lParam = (LPARAM)szPath;
    shbi.lpfn = BrowseCallbackProc;
	
    OleInitialize(0);
	
    // show the browse-dialog!
    if((pidl = SHBrowseForFolder(&shbi)) != 0)
    {
        // convert the selected ITEMIDLIST back to a path
        if(SHGetPathFromIDList(pidl, szPath))
            success = TRUE;
	
        // free the ITEMIDLIST
        SHGetMalloc(&pMalloc);
        pMalloc->Free(pidl);
        pMalloc->Release();
    }

    OleUninitialize();
    return success;
}

The callback function looks like this.

int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData)
{
    if(uMsg == BFFM_INITIALIZED)
        SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData);

    return 0;
}

Note that the lpData value is passed straight to SendMessage. We know this is safe to do because we set the corresponding lParam field in the BROWSEINFO structure, which points to the path we want.

Explorer Context Menu

Many applications like to add entries to the Shell context-menu in Explorer. The common way to do this is by writing a Shell Extension DLL in ATL/COM, but this is alot of work when only a simple text-entry with no bitmaps/icons is required.

The code below shows how to add a “Open with MyProgram” entry to all file-types in the system, so that when a file is right-clicked and the “Open with…” option selected, the application will be spawned with the selected filename passed as the first commandline argument:

#define CONTEXT_APP_LOC _T("Software\\Classes\\*\\shell\\Open With MyApp")
#define CONTEXT_CMD_LOC _T("Software\\Classes\\*\\shell\\Open With MyApp\\command")
void AddToExplorerContext()
{
    TCHAR szAppPath[MAX_PATH];
    TCHAR szDefaultStr[MAX_PATH];

    GetModuleFileName(0, szAppPath, MAX_PATH);
    wsprintf(szDefaultStr, _T("\"%s\" \"%%1\""), szAppPath);    
    RegSetValue( HKEY_CURRENT_USER, 
                 CONTEXT_CMD_LOC, 
                 REG_SZ, 
                 szDefaultStr, 
                 lstrlen(szDefaultStr) * sizeof(TCHAR)
           );
}

The code above sets the “command” key to the following value:

<application-path> "%1"

To remove the menu-item use the following function:

void RemoveFromExplorerContext()
{
    RegDeleteKey(HKEY_CURRENT_USER, CONTEXT_CMD_LOC);    
    RegDeleteKey(HKEY_CURRENT_USER, CONTEXT_APP_LOC);
}

Force Window onto visible display

If you use dual (or even triple/quad) displays then you have undoubtably encountered the following situation: You change the physical order of your displays, or otherwise reconfigure the logical ordering using your display software. This sometimes has the side-effect of changing your desktop coordinates from zero-based to negative starting coordinates (i.e. the top-left coordinate of your desktop changes from 0,0 to -1024,-768).

This effects many Windows programs which restore their last on-screen position whenever they are started. Should the user reorder their display configuration this can sometimes result in a Windows program subsequently starting in an off-screen position (i.e. at a location that used to be visible) - and is now effectively invisible, preventing the user from closing it down or otherwise moving it back on-screen.

void ForceVisibleDisplay(HWND hwnd)
{
    RECT rect;
    GetWindowRect(hwnd, &rect);

    // check if the specified window-rectangle is visible on any display
    if(NULL == MonitorFromRect(&rect, MONITOR_DEFAULTTONULL))
    {
        HMONITOR hMonitor;
        MONITORINFO mi = { sizeof(mi) };

        // find the nearest display to the rectangle
        hMonitor = MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST);        

        GetMonitorInfo(hMonitor, &mi);

        // center window rectangle
        rect.left = mi.rcWork.left + ((mi.rcWork.right - mi.rcWork.left) - (rect.right-rect.left)) / 2;
        rect.top = mi.rcWork.top + ((mi.rcWork.bottom - mi.rcWork.top) - (rect.bottom-rect.top)) / 2;        

        SetWindowPos(hwnd, 0, rect.left, rect.top, 0, 0, SWP_NOACTIVATE|SWP_NOZORDER|SWP_NOSIZE);
    }
}

The ForceVisibleDisplay function can be called at program start-time right after the main window has been created and positioned ‘on-screen’. Should the window be positioned in an off-screen position, it is forced back onto the nearest display to its last position. The user will be unaware this is happening and won’t even realise to thank you for keeping their user-interface visible, even though they changed their display settings. Thanks to Norman Diamond for spottng the problems with the last version, this one will hopefully work a little better!

Change the System Highlight colour

It is possible to change the “system highlight” colour on a per-application basis. For example, suppose you wanted to change the Edit control’s selection colour just for your application, so that it matched the rest of your custom controls. Using this technique it is possible to do this.

#include <windows.h>
#include <detours.h>

DETOUR_TRAMPOLINE(
    COLORREF WINAPI GetSysColorTrampoline(UINT),
    GetSysColor
);

COLORREF WINAPI GetSysColorDetour(UINT uIndex)
{
    if(uIndex == COLOR_HIGHLIGHT)
        return RGB(255, 0, 255); // your_rgb_value_here
    else
        return GetSysColorTrampoline(uIndex);
}

void Init()
{    
    DetourFunctionWithTrampoline(
        (PBYTE)GetSysColorTrampoline,
        (PBYTE)GetSysColorDetour);
}

The technique works by installing a Detour on the GetSysColor API (for the current application only). When the edit control draws it’s selection colour, it calls this routine and the detour above intercepts it and returns a different value. You need the Detours library from microsoft for this to compile.

New XP-style ListViews

To enable the new XP visual style in ListViews (the translucent-blue selection rectangle) you need to do two things. Firstly, add an XP-Theme manifest to your executable to get the new common-controls 6.0 library. Then enable “double-buffering” mode for the ListView:

ListView_SetExtendedListViewStyle(hwndListView, LVS_EX_DOUBLEBUFFER);

ListView Groups

To use ListView groups you must first enable group-view mode:

ListView_EnableGroupView(hwndList, TRUE);

Adding a “group” requires the following code:

void AddListViewGroup(HWND hwndList, WCHAR *szText, int iGroupId)
{
    LVGROUP lvgrp = { sizeof(lvgrp) };

    lvgrp.mask = LVGF_HEADER | LVGF_GROUPID | LVGF_ALIGN;
    lvgrp.pszHeader = szText;
    lvgrp.cchHeader = wcslen(lvgrp.pszHeader);
    lvgrp.iGroupId = iGroupId;
    lvgrp.uAlign = LVGA_HEADER_CENTER;

    ListView_InsertGroup(hwndList, 0, &lvgrp);
 }
AddListViewGroup(hwndList, L"Group Title", 12345);

New ListView items must be added into a specific group otherwise they don’t appear.

void AddItemToListGroup(HWND hwndList, TCHAR *szItem, int iItemIdx, int iImageIdx, int iGroupId)
{
    LVITEM lvitem;

    lvitem.mask = LVIF_IMAGE | LVIF_TEXT | LVIF_GROUPID;
    lvitem.iItem = iItemIdx;
    lvitem.iSubItem = 0;
    lvitem.pszText = szItem;
    lvitem.iImage = iImageIdx;
    lvitem.iGroupId = iGroupId; ListView_InsertItem(hwnd, &lvitem);
}
AddItemToListViewGroup(hwndList, L"Item 0", 0, 77, 12345);
AddItemToListViewGroup(hwndList, L"Item 1", 1, 88, 12345);

User Profile directory

Obtain the path of where the user-profiles are stored. i.e. "C:\Documents And Settings" or "C:\WINNT\Profiles".

//
// Find out where the user-profile directory is located
// i.e. C:\Documents And Settings
// C:\WINNT\Profiles
//
BOOL GetProfilePath(TCHAR *szProfilePath, ULONG nLength)
{
    HKEY hKey;
    TCHAR szPath[MAX_PATH];
    ULONG nPathLen = sizeof(szPath);

    TCHAR *szProfileList = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList";    

    if(ERROR_SUCCESS != RegOpenKeyEx(HKLM, szProfileList, 0, KEY_READ, &hKey))
        return FALSE;    

    if(ERROR_SUCCESS == RegQueryValueEx(hKey, "ProfilesDirectory", 0, REG_SZ, szPath, &nPathLen))
    {
        ExpandEnvironmentStrings(szPath, szProfilePath, nLength);
        RegCloseKey(hKey);
        return TRUE;
    }
    else
    {
        RegCloseKey(hKey);
        return FALSE;
    }
}

Parent Process Id

Find out which process started you with this handy routine (Windows NT/2000/XP only).

ULONG GetParentPid(HANDLE hProcess)
{
    PROCESS_BASIC_INFORMATION pbi = { 0 };

    // Query the ProcessBasicInformation(0)
    ZwQueryProcessInformation(hProcess, 0, &pbi, sizeof(pbi), 0);

    return pbi.InheritedFromUniqueProcessId;
}

The code above makes use of an undocumented “Native API” called ZwQueryInformationProcess. This API is exported by NTDLL.DLL so you can easily locate it using GetModuleHandle / GetProcAddress. The function-pointer prototype is shown below:

ULONG (WINAPI * ZwQueryInformationProcess) (
                IN HANDLE ProcessHandle,
                IN ULONG ProcessInformationClass,
                OUT PVOID ProcessInformation,
                IN ULONG ProcessInformationLength,
                OUT PULONG ReturnLength OPTIONAL );

The PROCESS_BASIC_INFORMATION structure is also not a standard PlatformSDK structure. It’s definition is shown here:

typedef struct  
{
     ULONG ExitStatus;
     PVOID PebBaseAddress;
     ULONG AffinityMask;
     ULONG BasePriority;
     ULONG_PTR UniqueProcessId;
     ULONG_PTR InheritedFromUniqueProcessId; 

} PROCESS_BASIC_INFORMATION;

Note that the function/structure definitions I have shown are slightly different to how they appear in other sources - I have changed all of the “NT” types to simple ULONG/PVOID types to make compilation easier.

// example:
DWORD dwParentId = GetParentPid(GetCurrentProcess());

Process Id from HANDLE

The exact same technique can be used to obtain the process ID from a process-handle, given a HANDLE to any process in the system. To do this, access the UniqueProcessId member inside the PROCESS_BASIC_INFORMATION structure, instead of the parent-id.

ULONG ProcessIdFromHandle(HANDLE hProcess)
{
    PROCESS_BASIC_INFORMATION pbi = { 0 };

    // Query the ProcessBasicInformation(0)
    ZwQueryProcessInformation(hProcess, 0, &pbi, sizeof(pbi), 0);

    return pbi.UniqueProcessId;
}
DWORD dwProcessId = ProcessIdFromHandle(hProcess);

Find where the user-kernel divide is located

Usually the system address-range starts at 0x80000000. However it is dangerous to assume this because of the ability to boot Windows with the /3Gb switch. The snippet below shows how to retrieve the “system range start” for the current system.

ULONG GetSystemRangeStart()
{
    ULONG nRangeStart = 0;

    // get the SystemRangeStartInformation (50) value
    ZwQuerySystemInformation(50, &nRangeStart, sizeof(ULONG));

    return nRangeStart;
}

argv[] and argc from a Windows program

Many people don’t realise that it is possible to access the “traditional” argv[] commandline from a Windows GUI program. The Visual C compiler provides the program’s commandline as global - in the __argc and __ argv variables. These are the ANSI (char*) commandline variables. To obtain the Unicode (WCHAR*) commandline the GetCommandLineW API can be called.

Combined together we can write a routine which returns the commandline:

TCHAR **GetArgvCommandLine(int *argc)
{
#ifdef UNICODE
    return GetCommandLineW(argc);
#else
    *argc = __argc;
    return __argv;
#endif
}

The following program shows how to access argv and argc directly from WinMain:

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR lpCmdLine, int nCmdShow)
{
    TCHAR *argv;
    int argc;

    // obtain command-line arguments in argv[] style array
    argv = GetArgvCommandLine(&argc);

    // process the argv array
    for(int i = 0; i < argc; i++)
    {
        OutputDebugString(argv[i]);
    } return 0;
}

Detect if an executable was started from a Short-Cut

This tip is copied from another article I wrote - Undocumented CreateProcess - and shows you is how any win32 application can detect if it was started from a shortcut (i.e. by double-clicking a shortcut link), or directly via Windows explorer or the Run dialog. This method also allows you to obtain the path of the shortcut link as well.

There is an undocumented STARTUPINFO flag which I have called STARTF_TITLESHORTCUT. This bit-flag has the value 0x800. Windows sets this flag when an executable has been started via a short-cut. So it is possible for any program to check if it was started this way by examining it’s own STARTUPINFO structure to see if this flag was specified:

// Returns TRUE if started through a shortcut, FALSE if not. // Also returns the name of the shortcut file (if any)
BOOL GetShortcutName(TCHAR *szLinkName, UINT nLinkNameSize)
{    
    STARTUPINFO si = { sizeof(si) };

    GetStartupInfo(&si);

    if(si.dwFlags & STARTF_TITLESHORTCUT)
    {
        lstrcpyn(szLinkName, si.lpTitle, nLinkNameSize);
        return TRUE;
    }

    return FALSE;
}

It is a simple matter to check for this undocumented flag, but another important piece of information should be explained. When the STARTF_TITLESHORTCUT flag is set, the lpTitle member of STARTUPINFO points to a string which contains the full path to the shortcut file used to start the current executable.

So imagine that there is a short-cut on your desktop which launches notepad.exe. When notepad.exe starts, it’s STARTUPINFO::lpTitle contains the following text:

C:\Documents and Settings\James\Desktop\Notepad.lnk

The following snippet shows how to use GetShortcutName :

char linkName[MAX_PATH];

if(GetShortcutName(&linkName, MAX_PATH))
{
    printf("started from %s\n", linkName);
}

Resolve shortcut

Sometimes it is necessary to resolve a shell-shortcut link to the referenced file. The Win32 C-code below shows how.

//
//	Resolve a ShellLink (i.e. c:\path\shortcut.lnk) to a real path
//
BOOL ResolveShortcut(TCHAR *pszShortcut, TCHAR *pszFilePath, int nPathLen)
{
    IShellLink * psl;
    SHFILEINFO info;
    IPersistFile *ppf;
    *pszFilePath = 0;   

    // assume failure
    if((SHGetFileInfo(pszShortcut, 0, &info, sizeof(info), SHGFI_ATTRIBUTES) == 0))
        return FALSE;

    // not a shortcut?
    if(!(info.dwAttributes & SFGAO_LINK))
    {
        lstrcpyn(pszFilePath, pszShortcut, nPathLen);
        return TRUE;
    }

    // obtain the IShellLink interface
    if(FAILED(CoCreateInstance(&CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, &IID_IShellLink, (LPVOID*)&psl)))
        return FALSE;

    if (SUCCEEDED(psl->lpVtbl->QueryInterface(psl, &IID_IPersistFile, (LPVOID*)&ppf)))
    {
        if (SUCCEEDED(ppf->lpVtbl->Load(ppf, pszShortcut, STGM_READ)))
        {
            // Resolve the link, this may post UI to find the link
            if (SUCCEEDED(psl->lpVtbl->Resolve(psl, 0, SLR_NO_UI )))
            {
                psl->lpVtbl->GetPath(psl, pszFilePath, nPathLen, NULL, 0);
                ppf->lpVtbl->Release(ppf);
                psl->lpVtbl->Release(psl);
                return TRUE;
            }
        }

        ppf->lpVtbl->Release(ppf);
    }

    psl->lpVtbl->Release(psl);
    return FALSE;
}

Return values from a DialogProc

Returning a value from a dialog-procedure is not as simple as a normal window-procedure. A DialogProc only returns TRUE/FALSE to indicate to Windows whether or not the message was processed. To return a ‘real’ value from a DialogProc requires calling the SetWindowLongPtr API with the DWLP_MSGRESULT parameter.

A common example is processing the WM_CTLCOLOREDIT message inside a dialog-box:

case WM_CTLCOLOREDIT:

    // set foreground/background colours as normal
    SetTextColor(hdc, rgbForeground);
    SetBkColor(hdc, rgbBackground);

    // 'return' the brush from the dialog-procedure
    SetWindowLongPtr(hwndDlg, DWLP_MSGRESULT, (LONG_PTR)hBrush);

    // tell Windows that we processed this dialog-message
    return TRUE;

The SetWindowLong API is used in this situation to return the HBRUSH back to Windows.

Small icons for titlebars

Setting the icon to appear in a window’s titlebar is not as simple as calling LoadIcon, because even though your icon resource might contain a 16x16 image, LoadIcon will only load the 32x32 image size. The following snippets shows how to call RegisterClassEx using LoadImage to set both the small and large icons for a window’s titlebar:

void InitWindow()
{
    WNDCLASSEX wcx;

    ....
    wcx.hIcon = LoadImage(hInst, MAKEINTRESOURCE(IDI_APP), IMAGE_ICON, 32, 32, LR_CREATEDIBSECTION);
    wcx.hIconSm = LoadImage(hInst, MAKEINTRESOURCE(IDI_APP), IMAGE_ICON, 16, 16, LR_CREATEDIBSECTION);

    RegisterClassEx(&wcx);
}

The alternative method is to use the SendMessage function after the window has been created, and use the WM_SETICON message, specifying the ICON_BIG and ICON_SMALL parameters where appropriate.