Search code examples
c#winformspinvoketitlebar

Extend another application's title bar


I'm working on an open-source .NET clone (GitHub) of DeskPins by Elias Fotinis (direct download off Google Drive). Its main function is to make other windows always-on-top. When they are on top, a pin icon is added to the title bar, which looks like this:

enter image description here

The pin icon moves with the window during drag and drop, and really looks like a part of it. It even responds to click event, which cancels always on top status and removes the icon from display.

Question. Is it possible to implement something like this in C# (and p/invoke, I'd assume)?

Research

  1. I tried to run this project.

It's a WPF demo, which is supposed to add a custom control to the title bar. Does not seem to work on Windows 7 x64. Not sure if it's the OS or otherwise. Problem - z-order is not consistent, title bar appears on top of other windows as well, and it does not move with the window, it tries, but lots of visual artifacts and flicker.

  1. Tried to apply this solution to #1:

Basically replacing relevant call to SetWindowLong with this pattern:

SetWindowLong(guestHandle, GWL_STYLE, GetWindowLong(guestHandle, GWL_STYLE) | WS_CHILD);
SetParent(guestHandle, hostHandle);

This change broke everything, so nothing was added to the title bar. Could be that it's not meant for title bars, only for the user area of the form.

In any case, if there is a simple solution, please share your wisdom. If not, I would appreciate any hints and/or links for me to investigate the topic further.


Solution

  • Here's how I've done this in DeskPins.

    Each pin is a popup window with a custom HRGN defining its shape. I did experiment with injecting a DLL into processes using hooks and drawing on the caption, but that was too messy for me back then.

    DeskPins creates the pin using WS_POPUP, WS_EX_TOPMOST and WS_EX_TOOLWINDOW, with 0 as the parent window. Then it immediately sends it a message (WM_PIN_ASSIGNWND), passing the target window to be pinned and the polling rate in msec. After that, the pin runs independently from DeskPins and they only send information messages to each other.

    The pin handles WM_PIN_ASSIGNWND by setting the target window as its parent, making it top-most and starting a polling timer. To set the parent it uses:

    SetWindowLong(hPin, GWL_HWNDPARENT, (LONG)hPinned);
    

    Note that Microsoft warns against setting the parent like this and instead suggests using SetParent(). However, SetParent() does some internal processing which prevents it from working across processes. Using SetWindowLong(GWL_HWNDPARENT) essentially tricks the window manager to accept this. It's a bit of a hack, but, hey... it works.

    The polling timer runs continuously and tests whether the target window is destroyed, hidden, moved or had its WS_EX_TOPMOST flag changed and responds appropriately.