Creating a custom caption bar and border at runtim

2019-06-24 00:52发布

问题:

I'm working on a rather large project that uses custom forms and would like to draw the non-client area of these forms myself. I can't use vcl-styles because many of the forms need to have a caption bar and border in a color picked at run time and as far as I know, styles are application wide by design.

What I have accomplished so far is drawing the caption bar and border, drawing on the caption, disabling the minimize, maximize and close buttons and replacing them with my own. I achieved this by intercepting the WM_NCPaint and WM_NCACTIVATE messages and calling my own procedure after the inherited keyword when handling WM_NCACTIVATE and sending a WM_ACTIVATE message without handling the WM_NCPAINT message as such:

SendMessage(Handle, WM_NCACTIVATE, ORD(active), -1);

The reason I did this is because I couldn't get the TMainMenu to paint itself consistently, after stepping through the code it seemed that the WM_NCACTIVATE message being handled correctly paints the main menu. Another approach I tried there was calling UpdateItems on the main menu, but this didn't give any result.

I deactivated the top right buttons by handling the WM_NCHITTEST message as such:

procedure TBasicForm.WMNCHITTEST(var message : TMessage);
begin
  inherited;
  case message.Result of
    HTMINBUTTONE, HTMAXBUTTON, HTCLOSE: message.Result := HTCAPTION;
  end;
end;

I got my own buttons (which I draw in the procedure I call when Handling WM_NCACTIVATE) by handling WM_NCLBUTTONDOWN, this is not a perfect solution but, can easily be improved on. (I trust I do not need to elaborate on this.

So far this sounds pretty good, however.

  • There is quite a lot of flickering when switching focus between forms certain ways.
  • Sometimes the original top right buttons show up, though they don't react to the mouse anymore.
  • When closing a form, The caption bar (and only the caption) bar reverts to its old appearance.

The direct question is, how do I solve these three issues? It might be that I'm going about this entirely the wrong way though, in which case the question is, how can I achieve a custom drawn caption bar and border, preferably without meddling with the functionality of the borders and caption bar too much and having the a main menu be drawn properly?

As I said, it's a rather large project with many forms, so changing things around in the form designer is one of the last things I'd consider doing.

To Reproduce the problems I'm experiencing, create a new form and handle WM_NCHITTEST, WM_NCACTIVATE and WM_NCPAINT as I described earlier.

...
procedure WMNCHITTEST(var message : TMessage); message WM_NCHITTEST;
procedure WMNCACTIVATE(var message : TMessage); message WM_NCACTIVATE;
procedure WMNCPAINT(var message : TMessage); message WM_NCPAINT;
...
implementation
...
procedure TBasicForm.WMNCHITTEST(var message : TMessage);
begin
  inherited;
  case message.Result of
    HTMINBUTTONE, HTMAXBUTTON, HTCLOSE: message.Result := HTCAPTION;
  end;
end;

procedure TBasicForm.WMNCACTIVATE(var message : TMessage);
begin
  inherited;
  Canvas.Brush.Style := bsSolid;
  Canvas.Brush.Color := clRed;

  Canvas.Rectangle(0, 0, Width, GetSystemMetric(SM_CYCAPTION) + GetSystemMetric(SM_CYBORDER));
  Canvas.Rectangle(0, 0, GetSystemMetric(SM_CXBORDER), Height);
  Canvas.Rectangle(Width - GetSystemMetric(SM_CXBORDER), 0, Width, Height);
  Canvas.Rectangle(Width - GetSystemMetric(SM_CXBORDER), Heigth - GetSystemMetric(SM_CYBORDER), Width, Height);
end;

procedure TBasicForm.WMNCPAINT(var message : TMessage);
begin
  SendMessage(Handle, WM_NCACTIVATE, ORD(active), -1);
end;
...

Now, add a second form to the project, make sure both forms are created and switch focus between the two forms repeatedly (also try clicking the second form, then click the custom drawn caption bar of first form), this should result in some flickering and the close, min and max button showing up. closing the form (by pressing alt + f4) should briefly show the original caption bar.

回答1:

Write a proper class to paint the non client area of a form require a lot of work, you are already handling some of the basic windows messages involved but there a lot more. Based on my experience these are my recommendations.

A. There is quite a lot of flickering when switching focus between forms certain ways.

Q. This issue can have many causes, but the main is use several calls to the draw method on the canvas, you can overcome this using a bitmap buffer (TBitmap) to draw all the title bar to the canvas of the bitmap and finally call the Canvas.Draw just once passing the bitmap.

A. Sometimes the original top right buttons show up, though they don't react to the mouse anymore.

See the answer to the next question.

A. When closing a form, The caption bar (and only the caption) bar reverts to its old appearance.

Q This is because you need to invalidate the NC Area of the form when the form is restored or resized , so you must add support for some additional messages like WM_WINDOWPOSCHANGING, WM_SIZE, WM_MOVE, WM_NCMOUSEMOVE, WM_NCLBUTTONDOWN, WM_NCRBUTTONDOWN and so on

To avoid all this work you can use the VCL Styles, for this you must override the TFormStyleHook class and implement a set of custom style hooks and apply in the forms which you want to custom the title bar using the RegisterStyleHook method like so

TStyleManager.Engine.RegisterStyleHook(TMyForm1, TMyCustomformStyleHook1);
TStyleManager.Engine.RegisterStyleHook(TMyForm2, TMyCustomformStyleHook2);