Академический Документы
Профессиональный Документы
Культура Документы
Maybe the above picture might look too ambicious but as they say, the more difficult the
challenge the better you skills improve.
Hope I will get near to it, so lets start.
To give it the power to move the window, we will modify the MouseDown event
procedure TMetroGUI.lblAppTitleMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
begin
ReleaseCapture;
Perform(WM_SYSCOMMAND, $F012, 0);
end;
As you can see, we release the mousedown event so no click event will be fired, and we
perform a window system command, the undocumented $F012 that is send to every window
when a Window move event is called.
B1 := GetBValue(C1) ;
C2
R2
G2
B2
:=
:=
:=
:=
ToColor;
GetRValue(C2) ;
GetGValue(C2) ;
GetBValue(C2) ;
dr := (R2-R1) / Rect.Right-Rect.Left;
dg := (G2-G1) / Rect.Right-Rect.Left;
db := (B2-B1) / Rect.Right-Rect.Left;
cnt := 0;
for X := Rect.Left to Rect.Right-1 do
begin
R := R1+Ceil(dr*cnt) ;
G := G1+Ceil(dg*cnt) ;
B := B1+Ceil(db*cnt) ;
Canvas.Pen.Color := RGB(R,G,B) ;
Canvas.MoveTo(X,Rect.Top) ;
Canvas.LineTo(X,Rect.Bottom) ;
inc(cnt) ;
end;
end;
procedure GradVertical(Canvas:TCanvas; Rect:TRect; FromColor, ToColor:TColor) ;
var
Y:integer;
dr,dg,db:Extended;
C1,C2:TColor;
r1,r2,g1,g2,b1,b2:Byte;
R,G,B:Byte;
cnt:Integer;
begin
C1 := FromColor;
R1 := GetRValue(C1) ;
G1 := GetGValue(C1) ;
B1 := GetBValue(C1) ;
C2
R2
G2
B2
:=
:=
:=
:=
ToColor;
GetRValue(C2) ;
GetGValue(C2) ;
GetBValue(C2) ;
dr := (R2-R1) / Rect.Bottom-Rect.Top;
dg := (G2-G1) / Rect.Bottom-Rect.Top;
db := (B2-B1) / Rect.Bottom-Rect.Top;
cnt := 0;
for Y := Rect.Top to Rect.Bottom-1 do
begin
R := R1+Ceil(dr*cnt) ;
G := G1+Ceil(dg*cnt) ;
B := B1+Ceil(db*cnt) ;
Canvas.Pen.Color := RGB(R,G,B) ;
Canvas.MoveTo(Rect.Left,Y) ;
Canvas.LineTo(Rect.Right,Y) ;
Inc(cnt) ;
end;
end;
However, there is no shadow, and since Windows doesnt apply the normal shadow to a window
without style, we need to apply by ourselves, be it by using the so old simple shadow or creating
a layered window as a shadow. This last one well be a little difficult but doable, lets just start
with the old shadow one.
For this purpose we need to modify the form create params.
...
protected
procedure CreateParams(var Params: TCreateParams);override;
end;
...
implementation
...
procedure TMetroGUI.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.WindowClass.style := Params.WindowClass.style or CS_DROPSHADOW;
end;
We only need to enable the CS_DROPSHADOW flag and we now have a simple shadow.
Now it have a better look. At the end, Im planning to add a better shadow using another form.
And they will toggle the focus state an call the repaint procedure
procedure TMetroGUI.LostFocus(Sender: TObject);
begin
isFocused:=False;
Repaint;
end;
procedure TMetroGUI.SetFocus(Sender: TObject);
begin
isFocused:=True;
Repaint;
end;
But we need to modify the FormPaint procedure in order to get that effect
procedure TMetroGUI.FormPaint(Sender: TObject);
begin
if isFocused then
GradHorizontal(Canvas, ClientRect, $e7ded5,$e2e5df)
else
GradHorizontal(Canvas, ClientRect, $c8c0b8,$c0c3bd);
end;
else
GradHorizontal(Canvas, ClientRect, $c8c0b8,$c0c3bd);
with canvas do begin
Pen.Color:=$eeeeee;
MoveTo(0,0);
LineTo(0,ClientHeight-1);
LineTo(ClientWidth-1, ClientHeight-1);
LineTo(ClientWidth-1,0);
LineTo(0,0);
end;
end;
We just added, with canvas that draws the almost white line, you can change to any other
color of course
Resize borders
It is time to add a resize area, generally it will be located in the bottom-right part of the window.
Lets add a simple drawing on the right bottom area of our form adding to our formpaint
procedure
//lets draw a resize area in the rightbottom part
Brush.Color:=clwhite;
FillRect(rect(ClientWidth-4,ClientHeight-4,ClientWidth-2,ClientHeight-2));
FillRect(rect(ClientWidth-7,ClientHeight-4,ClientWidth-5,ClientHeight-2));
FillRect(rect(ClientWidth-10,ClientHeight-4,ClientWidth-8,ClientHeight-2));
FillRect(rect(ClientWidth-13,ClientHeight-4,ClientWidth-11,ClientHeight-2));
FillRect(rect(ClientWidth-4,ClientHeight-7,ClientWidth-2,ClientHeight-5));
FillRect(rect(ClientWidth-7,ClientHeight-7,ClientWidth-5,ClientHeight-5));
FillRect(rect(ClientWidth-10,ClientHeight-7,ClientWidth-8,ClientHeight-5));
FillRect(rect(ClientWidth-4,ClientHeight-10,ClientWidth-2,ClientHeight-8));
FillRect(rect(ClientWidth-7,ClientHeight-10,ClientWidth-5,ClientHeight-8));
FillRect(rect(ClientWidth-4,ClientHeight-13,ClientWidth-2,ClientHeight-11));
It is a simple way to draw a triangle area with separated dots as shown in the following picture
Now, it needs to respond a mousedown event that will perform the resize action
procedure TMetroGUI.FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
//let's resize if on resize area
if (X>ClientWidth-13) and (Y > ClientHeight-11) then
begin
ReleaseCapture;
Perform(WM_SYSCOMMAND,$F008,0);
end;
end;
The formMouseDown procedure shown above limits the mouse area to that specific area,
sending a system command corresponding to the resize width & height together. The result is:
As you can see, it needs somethign more to make it better, since the painting fails, so we just
need to call repaint on resize event.
procedure TMetroGUI.FormResize(Sender: TObject);
begin
Repaint;
end;
As you can see, the resize procedure can resize the window too much that the user can turn the
form like a one pixel form. So we need to limit the minimum width and height to avoid that ugly
behaviour.
So on FormCreate, we define those constraints
procedure TMetroGUI.FormCreate(Sender: TObject);
begin
Application.OnDeactivate:= LostFocus;
Application.OnActivate:= SetFocus;
Constraints.MinWidth:=400;
Constraints.MinHeight:=200;
end;
As you can see, first, we need to know the current window status, if it is already maximized,
then we will restore it, otherwise, we will maximize it.
However, this perform function is okay, but we need to modify something because it will
maximize to the entire desktop screen size without respecting the working area (all window
minus the taskbar area usually).
As we are sending a syscommand event, we need to modify this event, so lets add it to the
form private area
private
{ Private declarations }
isFocused: Boolean;
procedure LostFocus(Sender: TObject);
procedure SetFocus(Sender: TObject);
procedure WMSysCommand (var Msg: TWMSysCommand); message WM_SYSCOMMAND;
end;
As you see, we intercept the SC_MAXIMIZE message and verify if the current window state is
different than wsMaximized then we change the windowState to wsMaximized and resize
according to the screen work area rect (keep in mind this will only work on one monitor setups,
that would be modified later to improve it for dual o multimonitor). Back to the code, we set the
actual form bounds to the size of the work area rect, and after that we clear the msg result and
exit before giving the msg to the default window handler.
Now our form resizes correctly. In the following steps we will drop that approach with a better
one.
Saddly this style gives back the non bsnone borderstyle, i.e., it has the non wanted classic
windows border. However, this border style responds correctly to WinKey+ArrowKeys to resize
with AeroSnap feature. So we need to get rid again of this classic windows border style.
protected
procedure WndProc(var Message: TMessage);override;
procedure CreateParams(var Params: TCreateParams);override;
So we just added a new procedure that will take care of the Windows processes.
procedure TMetroGUI.WndProc(var Message: TMessage);
begin
if Message.Msg = WM_NCCALCSIZE then
begin
Message.Msg:= WM_NULL;
end;
Inherited WndProc(Message);
end;
There we will modify the WM_NCCALCSIZE message which is used to determine the border
style size, and with it the window manager draws the classic border. We change that msg to 0
(WM_NULL) as when bsnone borderstyle. And for the other messages, we inherit them.
But, now we see a window resize bug when we Snap to the top screen to maximize it
If you dont see it, it is the application title bar reduce size, if you compare to the previous
snapshot of the maximize event, you will notice that the titles caption is located a little bit down.
Before proceeding, we will get rid of WMSysCommand procedure we wrote before, since it is
not needed anymore because we gave the almost correct maximize event with the AeroSnap
feature.
And to fix the bad maximize effect, we will copy the old WMSysCommand procedures to
the FormResizeevent.
procedure TMetroGUI.FormResize(Sender: TObject);
begin
if (WindowState = wsMaximized)then
begin
with Screen.WorkAreaRect do
MetroGUI.SetBounds(Left, Top, Right - Left-1, Bottom - Top-1);
end;
Repaint;
end;
As in the previous procedure, we need to adapt it for multimonitor setups. Well add it later.
Were good till here. However, one thing drives to another one. The new issue due to WinSnap
support is that the old system buttons re appear when we click over its area.
So we just added and not WS_SYSMENU to the Params.Style. However, there will not be a
Alt-Space application context menu. But in order to give our app the same experience as with a
normal form, we can use a tpanel set align to alClient and the lblAppTitle move inside it, finally
the resize mouse area moved to that panel mousedown event.
Clear the TPanel bevelouter to bvNone and only bypass the mouse down event
procedure TMetroGUI.Panel1MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
FormMouseDown(Sender,Button, Shift,X,Y);
end;
Easy, aint it, just get rid of the Panel caption to erase that Panel1 string on our form.
...
with canvas do begin
...
//let's paint the buttons
png:=TPngImage.Create;
try
png.LoadFromResourceName(HInstance,'PNGCLOSE');
Draw(ClientWidth-22,8,png);
png.LoadFromResourceName(HInstance, 'PNGMAX');
Draw(ClientWidth-44,8,png);
png.LoadFromResourceName(HInstance, 'PNGMIN');
Draw(ClientWidth-66,8,png);
finally
FreeAndNil(png);
end;
end;
end;
This has been added to our existing FormPaint procedure. Make sure to include at Uses clause
the PNGImage unit.
And to interact with mouse over and mouse out, we will use the event MouseEnter and
MouseLeave
procedure TMetroGUI.imgBtnCloseMouseEnter(Sender: TObject);
var
png: TPngImage;
begin
png := TPngImage.Create;
try
png.LoadFromResourceName(HInstance, 'PNGCLOSEON');
imgBtnClose.Picture.Assign(png);
finally
png.Free;
end;
end;
procedure TMetroGUI.imgBtnCloseMouseLeave(Sender: TObject);
var
png: TPngImage;
begin
png := TPngImage.Create;
try
png.LoadFromResourceName(HInstance, 'PNGCLOSE');
imgBtnClose.Picture.Assign(png);
finally
png.Free;
end;
end;
As you can see, first we verify if our window is maximized to either draw the restore icon or the
maximize one.
Now, to make sure its icon (button pic) shows the correct one when resizing it via hotkey or
other ways, we will add to FormResize this simple procedure call
...
imgBtnResizeMouseLeave(Sender);
...
That will be enough to make it aware of resizing events and will show the correct button image.
Always taking into account the window state, specially for the resize button.
Multimonitor Support
If you have more than one monitor, you will see that it maximizes to only one of them. To avoid
that we need to figure it out how many monitors we have, and according to where our
application is, we maximize to that monitor.
This is a function that tells us where a specific X,Y coordinate is located, i.e., in which monitor.
function WhichMonitor(horizCenter,vertCenter: integer):integer;
var
I: Integer;
begin
result:=-1;
for I := 0 to Screen.MonitorCount-1 do
begin
if(screen.Monitors[I].Left<horizCenter)
and(screen.Monitors[I].Left+Screen.Monitors[I].Width>horizCenter)
and(Screen.Monitors[I].Top<vertCenter)
and(Screen.Monitors[I].Top+Screen.Monitors[I].Height>vertCenter)
then
result:=I;
end;
end;
So when resizing our form, we make sure that the center X,Y of our form is located between
those boundaries.
So we modify it to include the monitor support:
P:= ClientToScreen(Point(X,Y));
asm
mov ax, word(P.Y);
shl eax, 16;
mov ax, word(P.X);
mov MP, eax;
end;
SendMessage(Handle,WM_SYSMENU,0,MP);
end;
end;
For this purpose we first make sure we clicked with the right button of our mouse (this is hacky
approach since other user might have enabled the left handed feature that uses the oposite
buttons). Anyways, lets continue.
The X and Y values store the coordinates relative to our form, so we need to convert it to screen
coordinates, thankfully with ClientToScreen we can do that.
After that we place those new coordinates in a 32bits value, the first 16bits holding the Y value
and the last 16bits the X value. I dont know how to do it, so assembly might help
. And finally
we send a message to our application with the command SysMENU and MP holding the
coordinates where it will be shown.
Conclusion
Making a custom Delphi application that mimics the Metro Style of Zune is not a trivial work
without the knowledge of WINAPI tricks, and taking into consideration every aspect that a
normal application has. However, theyre not as difficult as it might look, with the right tools
everything is possible. I know there are many ways to achieve this pseudo skin, with VCL
components already built, but I found them the lack of WinSnap, and other features. With this
approach you have the entire control of your code.
However, our work until here is not complete, we need to add a shadow effect and the correct
icons as our main goal was the Metro Browser concept by Sputnik8, and of course adding the
WebBrowser support maybe with TWebBrowser or TChromium.
Finally, I would like to thank you for reading this walkthrough of building a Metro like application
with Delphi. Hope you liked it and hope it might be of use for your projects. It took me a lot of try
and error, and finally Ive come up with something Im satisfied by now.
Download sources
You can get this article source codes for free here: