4

Mike Lischke's TThemeServices subclasses Application.Handle, so that it can receive broadcast notifications from Windows (i.e. WM_THEMECHANGED) when theming changes.

It subclasses the Application object's window:

FWindowHandle := Application.Handle;
if FWindowHandle <> 0 then
begin
 // If a window handle is given then subclass the window to get notified about theme changes.
 {$ifdef COMPILER_6_UP}
    FObjectInstance := Classes.MakeObjectInstance(WindowProc);
 {$else}
    FObjectInstance := MakeObjectInstance(WindowProc);
 {$endif COMPILER_6_UP}
 FDefWindowProc := Pointer(GetWindowLong(FWindowHandle, GWL_WNDPROC));
 SetWindowLong(FWindowHandle, GWL_WNDPROC, Integer(FObjectInstance));
end;

The subclassed window procdure then does, as it's supposed to, WM_DESTROY message, remove it's subclass, and then pass the WM_DESTROY message on:

procedure TThemeServices.WindowProc(var Message: TMessage);
begin
  case Message.Msg of
     WM_THEMECHANGED:
        begin
               [...snip...]
        end;
     WM_DESTROY:
        begin
          // If we are connected to a window then we have to listen to its destruction.
          SetWindowLong(FWindowHandle, GWL_WNDPROC, Integer(FDefWindowProc));
          {$ifdef COMPILER_6_UP}
             Classes.FreeObjectInstance(FObjectInstance);
          {$else}
             FreeObjectInstance(FObjectInstance);
          {$endif COMPILER_6_UP}
          FObjectInstance := nil;
        end;
  end;

  with Message do
     Result := CallWindowProc(FDefWindowProc, FWindowHandle, Msg, WParam, LParam);
end;

The TThemeServices object is a singleton, destroyed during unit finalization:

initialization
finalization
  InternalThemeServices.Free;
end.

And that all works well - as long as TThemeServices is the only guy who ever subclasses the Application's handle.

i have a similar singleton library, that also wants to hook Application.Handle so i can receive broadcasts:

procedure TDesktopWindowManager.WindowProc(var Message: TMessage);
begin
case Message.Msg of
WM_DWMCOLORIZATIONCOLORCHANGED: ...
WM_DWMCOMPOSITIONCHANGED: ...
WM_DWMNCRENDERINGCHANGED: ...
WM_DESTROY:
    begin
        // If we are connected to a window then we have to listen to its destruction.
        SetWindowLong(FWindowHandle, GWL_WNDPROC, Integer(FDefWindowProc));
        {$ifdef COMPILER_6_UP}
        Classes.FreeObjectInstance(FObjectInstance);
        {$else}
        FreeObjectInstance(FObjectInstance);
        {$endif COMPILER_6_UP}
        FObjectInstance := nil;
    end;
end;

with Message do
    Result := CallWindowProc(FDefWindowProc, FWindowHandle, Msg, WParam, LParam);

And my singleton is similarly removed when the unit finalizes:

initialization
   ...
finalization
    InternalDwmServices.Free;
end.

Now we come to the problem. i can't guarantee the order in which someone might choose to access ThemeServices or DWM, each of which apply their subclass. Nor can i know the order in which Delphi will finalize units.

The subclasses are being removed in the wrong order, and there is a crash on application close.

How to fix? How can i ensure that i keep my subclassing method around long enough until the other guy is done after me is done? (i don't want to leak memory, after all)

See also


Update: i see Delphi 7 solves the bug by rewriting TApplication. ><

procedure TApplication.WndProc(var Message: TMessage);
...
begin
   ...
   with Message do
      case Msg of
      ...
      WM_THEMECHANGED:
          if ThemeServices.ThemesEnabled then
              ThemeServices.ApplyThemeChange;
      ...
   end;
   ...
end;

Grrrr

In other words: trying to subclass TApplication was a bug, that Borland fixed when they adopted Mike's TThemeManager.

That very well may mean that there is no way to remove subclasses on TApplication in reverse order. Someone put that in the form of an answer, and i'll accept it.

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • Delphi 7's theme code is based on Mike Lischke's code. But of course, since they have the source, then there's no need for them to subclass. Is there a good reason why you can't use a modern Delphi? – David Heffernan Jan 06 '11 at 16:11
  • 3
    I don't agree at all that subclassing TApplication, as Mike did in his XP theme manager, was a bug. What else could he have done? What's more I regard that code as one of the greatest works of coding I've ever come across. In fact that's still one of the primary references for themed painting. Although there were some minor bugs they were few and far between and hardly surprising considering the complexity of what he was attempting. So, I'm sticking up for Mike, in response to your "Grrrr"!! – David Heffernan Jan 06 '11 at 20:00
  • i didn't mean to imply that subclassing `TApplication` was a bug - only that how he did it is buggy (it assumes nobody else before him subclassed TApplication, and that nobody else after him will either) – Ian Boyd Jan 14 '11 at 21:17
  • Why the delphi-t tag? What is delphi-t? – Jørn E. Angeltveit Apr 26 '11 at 08:59
  • @Jørn. Whoops. "T" is one key short of "5". Fixed the tag. Thank you. – Ian Boyd May 12 '11 at 17:02

4 Answers4

4

Change your code to call SetWindowSubclass, as the article you linked to advises. But that only works if everyone uses the same API, so patch Theme Manager to use the same technique. The API was introduced in Windows XP, so there's no danger that it's not available on the systems where it would be needed.

There should be no problem with patching Theme Manager. It's designed to support Windows XP, which Microsoft doesn't support anymore, and to support Delphi 4 through 6, which Borland doesn't support anymore. Since development has ceased on all relevant products, it is safe for you to fork the Theme Manager project without risk of falling behind due to future updates.

You're not really introducing a dependency. Rather, you're fixing a bug that's only present when both window-appearance libraries are in use at the same time. Users of your library don't need to have Theme Manager for yours to work, but if they wish to use both, they need to use Theme Manager with your patches applied. There should be little objection to that since they already have the base version, so it's not like they'd be installing an entirely new library. They'd just be applying a patch and recompiling.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
Rob Kennedy
  • 161,384
  • 21
  • 275
  • 467
  • The issue i still can't figure out, even when using SetWindowSubClass, is what to do when i want to unregister. The documentation doesn't mention it (the documentation doesn't mention much of anything - even the reason it exists) - but do i still have to keep my subclassing object around - as Raymond points out. – Ian Boyd Jan 06 '11 at 19:07
  • And some customers still use Windows 2000; so i'd would also introduce crashes – Ian Boyd Jan 06 '11 at 19:17
  • @Ian Themes and Dwm don't exist on Win2k either. So you wouldn't be calling this API when run on Win2k. – David Heffernan Jan 06 '11 at 19:19
  • @David That's a excellent point; i never thought of it that way. Although the library does handle DWM not being available, the first thing it does on `Create` is hook the window. i can just *not* hook the window if `SetWindowSublcass` isn't available - or even better: not hook if DWM isn't available. – Ian Boyd Jan 06 '11 at 19:33
  • @Ian @Rob SetWindowSubClass is certainly a good solution, but it's much more invasive than the solution that I offered. You objected to that because you'd have to change Mike Lischke's code, but you'll need to make a lot more changes to use SetWindowSubClass. Not that it's particularly hard to do so, but I just thought I would point that out. – David Heffernan Jan 06 '11 at 19:57
  • @Ian, MSDN has an overview about [subclassing windows](http://msdn.microsoft.com/en-us/library/bb773183.aspx), where it explains, "ComCtl32.dll version 6 supplied with Windows XP contains four functions that make creating subclasses easier and eliminate the disadvantages previously discussed." To unregister, just call `RemoveWindowSubclass`. Under the hood, I suspect `SetWindowSubclass` subclasses in the traditional way, but then manages it own list of subclasses where it's safe to remove from the middle. Call `DefSubclassProc` instead of directly calling the previous window proc. – Rob Kennedy Jan 06 '11 at 20:01
  • @Rob I hope you don't take offence to my last comment, I don't mean to knock what you wrote and I did give you an up-vote! – David Heffernan Jan 06 '11 at 20:05
  • 1
    Oh damn...i realized that SetWindowSubclass is in `CommCtrl32.dll` version 6. The only way i get version 6 is by manifesting my application to opt into version 6. If i do that i will get version 6 of the common controls. i'm using Delphi 5, which used the common controls improperly, which causes crashes. Mike tried to write `TThemeManager` to patch all Borland's wrappers- but there's still bugs it can't fix. So i can't use that :( – Ian Boyd Jan 06 '11 at 20:17
  • @ian looks like a delphi upgrade is needed – David Heffernan Jan 07 '11 at 13:26
  • 2
    @Ian: If you still care, the `SetWindowSubclass` class of functions still exist in older versions of CommCtrl32.dll, they're just exported by ordinal only. You have to do some digging, but you can use them if you really want to. [See here for details](http://vb.mvps.org/samples/HookXP/), albeit in VB 6-speak. – Cody Gray - on strike Feb 23 '11 at 09:46
  • 2
    Documentation says SetWindowSubclass is in Comctl32 version 5.8. – Rob Kennedy Feb 23 '11 at 15:52
  • @Cody Gray, @Rob Kennedy. You're right. Although, Rob, it does say minimum supported OS is XP/2003, while i have to support 2000. But a random 2000 machine i looked at has 5.81. So i'm sort of in a no-mans-land, where i can't officially use it or depend on it - but i might be able to use and depend on it. – Ian Boyd Feb 25 '11 at 17:40
  • But Ian, I thought we already established that Windows 2000 support doesn't really matter — at least for your purposes — because the theme- and DWM-related messages you want to intercept won't be sent. That OS supports neither themes nor DWM. – Rob Kennedy Feb 25 '11 at 21:13
2

Rather than subclassing the TApplication window, perhaps you can use AllocateHWnd() instead to receive the same broadcasts separately, since it is a top-level window of its own.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Hmmmmm. i never thought of that. Actually i had, but i didn't think some random message loop i setup would count. – Ian Boyd Jan 08 '11 at 23:51
  • If you call AllocateHWND() in the context of the main thread, it will use the main message loop, just as TApplication's window does. A thread content can run multiple top-level windows. – Remy Lebeau Jan 09 '11 at 08:29
  • Wait, will it use the main message loop code, or will it literally use the main message loop? Cause while the former is leveraging code, the latter means i'll be getting messages for any messages posted to any window in my application. i would just want messages being sent to *my* `AllocateHwnd` window. – Ian Boyd Jan 14 '11 at 21:20
  • An HWND is tied to the thread context that creates it. That thread needs a message queue and message processing loop. If you call AllocateHWnd() in the main thread context, its HWND will use the main thread's existing message queue and message loop (if you call AllocateHWnd() in a worker thread - which is not technically safe - then you have to implement your own message queue/loop in that thread). In that situation, the HWND will only receive messages that are explicitally posted/sent to it, and also top-level broadcasts. HWNDs do not receive messages belonging to other HWNDs. – Remy Lebeau Jan 15 '11 at 01:50
1

I think I would do the following:

  • Put a reference to ThemeServices in the initialization section of ThemeSrv.pas.
  • Put a reference to DwmServices in the initialization section of DwmSrv.pas (I'm guess the name of your unit).

Since units are finalised in reverse order from the order of initialisation, your problem will be solved.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Can't really change other people's units; or want to take a dependancy between them. – Ian Boyd Jan 06 '11 at 15:59
  • @Ian I can't imagine Mike would mind you doing that. I know I've modified his code to fix bugs in it. Finally I don't know what you mean about taking a dependency on other units. Your entire problem is one of dependencies. – David Heffernan Jan 06 '11 at 16:01
  • @Ian Regarding your comment, "Can't really change other people's units", you appear to have no qualms about copying the code from them. – David Heffernan Jan 06 '11 at 16:11
0

Why don't you just use the ApplicationEvents and be done with it. No need to messing around with subclassing. The other way is to create only one subclass and create multi-notify events and subscribe as many as you wish.

Cheers

APZ28
  • 997
  • 5
  • 4
  • 1
    Theme Manager supports Delphi version 4 through 6. `TApplicationEvents` is not available in all those versions. Besides, the `OnMessage` event gets called for *all* messages posted to *any* window in the application, whereas the desire is to only intercept the messages aimed at the single application window. Furthermore, `OnMessage` doesn't see *sent* messages, only *posted* messages. – Rob Kennedy Jan 06 '11 at 16:50