During the early development stages of the Terian ID Software and related products I was inspired by the “Creating User Experiences: Fundamental Design Principles” Pluralsight training course by Billy Hollis. I wanted my software to have specific theme or style that would make each product identifiable to the same family.
The problem being I am not an artistic type of person, and ideas I have in my head for what I like and want don’t necessarily translate well to an explanation (As anyone that has tried to do design work for me can attest to). Fortunately one of the demo’s Billy Hollis demonstrates has a form design which struck a chord with me. Basically the form was borderless, with the top right corner cut away.
So instead of a form like this…
We end up with one that looks like this…
This design seemed very simple yet elegant to me, so I decided to invest in it across the entire product range.
Custom Form Shape – Method 1:
Having an idea is one thing, however implementing it is another. Not having any experience with custom form shapes in Delphi I did a bit of research and one common method involves utilising a transparent image cut to the appropriate shape. This custom image is combined with a form that has its Color and TransparentColorValue properties set to a rarely used colour.
Starting with a blank form perform the following steps…
- Set Height/Width
- Set BorderStyle = bsNone
- Cleared BorderIcons
- Set Color = clTeal (Figured this was a Rarely Used Colour)
- Set TransparentColorValue = clTeal
- Set TransparentColor = True
These are the basic form properties to allow for a transparent form (when used with a transparent custom shaped image).
The next step is to add the form objects, which results in the following form structure…
The deconstructed form looks like this…
The secret ingredient is the imgCorner image which is a simple triangle with a transparent background. All other form objects square up nicely and cover the entirety of the form as the image below demonstrates…
This method works great initially, however it will become cumbersome and time consuming if you need to add more than half a dozen forms to an application.
Custom Form Shape – Method 1 – Resizing:
Some forms will require resizing functionality, and due to setting the BorderStyle property to bsNone we can no longer rely on the standard Windows resizing functionality. A workaround for this is to provide a custom control that mimics a Size Grip in the lower right corner of the form.
The TSjonesSizeGrip custom control is a simple 10×10 pixel control that descends from TGraphicControl and contains the Size Grip Image…
TSjonesSizeGrip contains an onMouseDown event which helps mimic form resizing…
procedure TfrmStyleViaMethod1.SjonesSizeGrip1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); const sc_DnRightsize = $f008; begin // 11/Mar/2013 - ADowling // --- // We temporarily set doublebuffering here to help reduce the amount of flickering that occurs when // resizing the form. // --- self.DoubleBuffered := True; ReleaseCapture; Perform(wm_SysCommand, sc_DnRightsize, 0); self.DoubleBuffered := False; end;
If all form objects are anchored appropriately then this is all that is required to enable form resizing. The only downside is that form resizing is only available by dragging the SizeGrip, where as standard dialogs can be resized using their borders.
Custom Form Shape – Method 2:
Wanting to find an easier and quicker method I did some additional research and came across a method that utilises the ‘CreatePolygonRgn’ and ‘SetWindowRgn’ Windows API calls. These calls would allow me to define a polygon region and then assign that as the visible region of a form.
As the desired custom shape is the same across all forms, the following Procedure is used to set the custom form shape
Procedure SetTerianFormShape(wnd : HWND; rect : TRect; rgn : HRGN); Var arPts : Array[0..4] of TPoint; Begin // Point 1 arPts[0].X := 0; arPts[0].Y := 0; // Point 2 arPts[1].X := rect.Width - 30; arPts[1].Y := 0; // Point 3 arPts[2].X := rect.Width; arPts[2].Y := 30; // Point 4 arPts[3].X := rect.Width; arPts[3].Y := rect.Height; // Point 5 arPts[4].X := 0; arPts[4].Y := rect.Height; rgn := CreatePolygonRgn(arPts, 5, WINDING); SetWindowRgn(wnd, rgn, TRUE); End;
The following procedure is used to reset the custom shape.
Procedure UnSetTerianFormShape(wnd : HWND); Begin SetWindowRgn(wnd, 0, False); End;
Each form requires its own Polygon Region handle, so we need to add a private variable to each form to contain this.
private { Private declarations } TerianFormShapeRGN : HRGN; // Variable to hold Polygon Region Handle
To set the custom shape for each form, the OnCreate Event calls the SetTerianFormShape Procedure…
procedure TfrmStyleViaMethod2.FormCreate(Sender: TObject); begin SetTerianFormShape(self.Handle, self.ClientRect, self.TerianFormShapeRGN); end;
This method of creating Custom Shaped Forms in Delphi is far simpler from a development perspective, as you can easily add new forms with varying sizes with no need to manipulate multiple shape objects.
The form structure is simplified to this…
The form Color property is set to the desired background colour, and no additional shape objects are required.
Custom Form Shape – Method 2 – Resizing:
Form resizing is handled in a similar way to method 1. However, due to the Window Region being set via a static polygon on form creation you need to reset the polygon region to allow the form to be visible outside the initial bounds of the form (in the event of increasing the form size). To perform this you simply Unset the Custom Form shape, perform the resize, and then reset the Form Shape like follows…
procedure TfrmStyleViaMethod2.SjonesSizeGrip1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); const sc_DnRightsize = $f008; begin CustomResizing := True; // Used to avoid FormResize Event Also Resetting Custom Form Shape. UnSetTerianFormShape(self.Handle); // 11/Mar/2013 - ADowling // --- // We temporarily set doublebuffering here to help reduce the amount of flickering that occurs when // resizing the form. // --- self.DoubleBuffered := True; ReleaseCapture; Perform(wm_SysCommand, sc_DnRightsize, 0); self.DoubleBuffered := False; SetTerianFormShape(self.Handle, self.ClientRect, self.TerianFormShapeRGN); CustomResizing := False; end;
Note: The additional variable called CustomResizing (defined in forms implementation) helps ensure the FormResize event does not attempt to reset the custom form shape when using the SizeGrip.
Similarly to the method 1 if all form objects are anchored appropriately no other processes are required to handle form resizing.
Custom Form Shape – Method 2 – Resizing Main Form Issue:
If method 2 is used to provide a custom form shape for the main form of an application, there is a small issue in regards to form resizing. If an applications shortcut is set to run the application Maximised then the custom form shape is not reset, and part of the form will not be displayed.
To avoid this issue you need to reset the custom form shape as part of the Form onResize event…
procedure TfrmMain.FormResize(Sender: TObject); begin // 26/Aug/2013 - ADowling // --- // We need to reset the TerianFormShape upon resizing, usually we do this as part of the custom // resizing performed in the SjonesSizeGrip1MouseDown method, however it is possible there are // other calls to FormResize (such as when an application is set to Run Maximized), in which case // we need to ensure the TerianFormShape is reset. // --- if Not CustomResizing then Begin UnSetTerianFormShape(self.Handle); SetTerianFormShape(self.Handle, self.ClientRect, self.TerianFormShapeRGN); CheckIfStretchRequired; end; end;
So far it works:
So far using method 2 has worked a charm, and from the end user perspective they seem quite happy. Granted method 1 also worked well, and from the end user perspective they noticed no difference, however the ease of creating new forms with method 2 is certainly far superior than messing around with multiple shape objects.
One item still on my to-do list, is testing these methods with font scaling to see how that might affect the forms and/or resizing.
Congratulations for the article and for the excellent work. I would just like to display a title bar wider, with a larger icon on my form, but I can not, could you help me?
Hi Carlos,
One approach might be to use a border-less form, and then simply have the icon and a fake title bar (i.e. just a shape) aligned to the top of the form, then you could use the on-mouse events for when people want to move your form around.
The downside to this approach is it would not take into account any themes or personalisation settings a user may have on their desktop.
Cheers
Anthony.