delphivcldelphi-xe3titlebarnonclient-area

Forcing a repaint of TMainMenu when handling WM_NCPAINT yourself


I'm custom painting my caption bar in a rather large application with many forms and have decided to try and do it the old-fashioned way by handling some messages myself and dong the drawing when I'm handling WM_NCPAINT.

The painting itself is going quite well and essentially works. One thing that won't work though is painting the TMainMenu. Either I have the default WM_NCACTIVATE handler draw the entire nonclient area (I have to Perform a WM_NCACTIVATE message inside my WM_NCPAINT handler) before painting over it, which causes flickering that seemingly can't be helped. Or I can try to have the default handler of WM_NCPAINT paint only the Rect containing the TMainMenu, which result in a smooth result, but doesn't repaint the menu.

My question is:

I've given it a few goes and I think I'm on the right way, but am hitting a wall; I don't have enough knowledge about what I'm doing and can't seem to find clear documentation about it. The most important part of my code is:

RedrawWindow(Handle, nil, MenuRegion, RDW_INVALIDATE or RDW_FRAME or RDW_NOERASE or RDW_ALLCHILDREN or RDW_UPDATENOW);

And I think it's exactly that what goes wrong. (or rather, the code where I calculate 'MenuRegion'. I just have no clue if it's going wrong because I'm using the wrong coordinate system or if it's because I'm going about this entirely the wrong way.

Here's a reduced version of my code that will compile and run 'as is' in delphi (xe3):

unit Unit3;
interface
uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.Menus;
type
  TForm3 = class(TForm)
  private
    FDrawMenu: Boolean;
    function CalcFrameRegion: HRGN;
    function CalcMenuRegion: HRGN;
    procedure DrawMenu;
    procedure FormFrame(minimal: Boolean = false);
    procedure WMNCActivate(var message: TWMNCActivate); message WM_NCACTIVATE;
    procedure WMNCPaint(var message: TMessage); message WM_NCPAINT;
    procedure WMSIZE(var message : TWMSIZE); message WM_SIZE;
    constructor Create(AOwner: TComponent); override;
  public
    { Public declarations }
  end;
var
  Form3: TForm3;
implementation
{$R *.dfm}
{ TForm3 }
function TForm3.CalcFrameRegion: HRGN;
var
  YCaption, YFrame, XFrame: Integer;
begin
  YCaption := GetSystemMetrics(SM_CYCaption);
  YFrame := GetSystemMetrics(SM_CYFRAME);
  XFrame := GetSystemMetrics(SM_CXFRAME);
  Result :=  CreateRectRgn(0, 0, YCaption + YFrame, Width);
  Result := Result +  CreateRectRgn(0, 0, Height, XFrame);
  Result := Result + CreateRectRgn(0, Height - YFrame, Width, Height);
  Result := Result + CreateRectRgn(Width - XFrame, 0, Width, Height);
end;
function TForm3.CalcMenuRegion: HRGN;
var
  XFrame, YFrame, YCaption, YMenu: Integer;
begin
  XFrame := GetSystemMetrics(SM_CXFRAME);
  YFrame := GetSystemMetrics(SM_CYFRAME);
  YCaption := GetSystemMetrics(SM_CYCAPTION);
  YMenu := GetSystemMetrics(SM_CYMENU);
  Result := CreateRectRgn(XFrame, YFrame + YCaption, Width - XFrame, YFrame + YCaption + YMenu);
end;
constructor TForm3.Create(AOwner: TComponent);
var
  testItem: TMenuItem;
begin
  inherited;
  // Creating a MainMenu and attatching it to the form.
  Menu := TMainMenu.Create(self);
  // The menu need san item.
  testItem := TMenuItem.Create(Menu);
  testItem.Caption := 'test';
  Menu.Items.Add(testItem);
  FDrawMenu := false;
end;
procedure TForm3.FormFrame(minimal: Boolean);
var
  YCaption, YFrame, XFrame: Integer;
begin
  YCaption := GetSystemMetrics(SM_CYCaption);
  YFrame := GetSystemMetrics(SM_CYFRAME);
  XFrame := GetSystemMetrics(SM_CXFRAME);
  Canvas.Handle := GetWindowDC(Handle);
  Canvas.Pen.Style := psClear;
  Canvas.Brush.Style := bsSolid;
  Canvas.Brush.Color := clRed;
  if not minimal then begin
    Canvas.Rectangle(0, 0, Width + 1, YCaption + YFRame + 1);
    Canvas.Rectangle(0, YCaption + YFRame, XFrame + 1, Height + 1);
    Canvas.Rectangle(XFrame, Height - YFrame, Width + 1, Height + 1);
    Canvas.Rectangle(Width - XFrame, YCaption + YFRame, Width + 1, Height - YFrame + 1);
  end;
end;
procedure TForm3.DrawMenu;
var
  MenuRegion: HRGN;
begin
  if Assigned(Menu) then begin
    MenuRegion := CalcMenuRegion;
    FDrawMenu := true; // Make sure the inherited handler gets called.
    // Force a redraw of the region defined by MenuRegion.
    RedrawWindow(Handle, nil, MenuRegion,
                  RDW_INVALIDATE or RDW_FRAME or RDW_NOERASE or RDW_ALLCHILDREN or RDW_UPDATENOW);
    FDrawMenu := false; // Use the FormFrame function again.
  end;
end;
procedure TForm3.WMNCActivate(var message: TWMNCActivate);
begin
  FormFrame;
  message.Result := 1; // This makes sure the message gets handled properly.
end;
procedure TForm3.WMNCPaint(var message: TMessage);
begin
  if FDrawMenu then
    inherited // Gets called when the Menu has to be drawn.
  else
    FormFrame; // Gets called in all other cases.
end;
procedure TForm3.WMSIZE(var message: TWMSIZE);
begin
  inherited;
  DrawMenu;
end;
end.

Solution

  • "How can I have a TMainMenu and only a TMainmenu repainted when I'm handling WM_NCPAINT myself?"

    In theory you can modify the region WM_NCPAINT is passed. Define a region that corresponds to the area of the menu bar, leave out the rest. The below is a proof of concept:

    procedure TForm3.WMNCPaint(var Message: TWMNCPaint);
    var
      R: TRect;
      MenubarInfo: TMenuBarInfo;
      MenuRgn: HRGN;
    begin
      FormFrame( whatever );
    
      MenubarInfo.cbSize := SizeOf(MenubarInfo);
      GetMenuBarInfo(Handle, OBJID_MENU, 0, MenubarInfo);
    
      MenuRgn := CreateRectRgnIndirect(MenubarInfo.rcBar);
      if Message.RGN <> 1 then
        DeleteObject(Message.RGN);
      Message.RGN := MenuRgn;
    
      inherited;
    end;
    


    In practice, you'll ultimately abandon this route. Note the test for '1' in the above sample. A pseudo handle for the update region is is not mentioned in the documentation. Yet the region handle is always '1' for a normal window. As a matter of fact non-client handling has never ever been properly documented. My guess is, that's because the OS itself does not play by the rules. Take for instance the fact that the NC area being painted during the default handling of WM_NCACTIVATE. What does activation have got anything to do with NC painting. Why does the OS paints behind your back?

    My suggestion is, take the route that VCL styles have taken. Once you are in drawing NC area, draw it all. Menu bar area is part of the non-client area.