Non-Rectangular Windows

One rather neat but relatively little-used feature of Windows which has been around for a couple of years is the ability to create windows - both forms and controls - that are not rectangular in shape. This facility is implemented by Windows itself rather than by .NET, and is available whether or not you are using managed code. The design of the Windows.Forms classes, however, makes non-rectangular windows particularly easy to accomplish in managed code. Although the feature doesn't seem to be used widely in third-party code, you can see the principle in almost every form in Windows XP - in the XP- style title bars with the round corners. We're now going to present a couple of examples that show how you can take advantage of this feature, as well as owner draw controls, to modify your user interface. You might want to do this for example if you're writing an application for home (rather than business use) by non-IT-professionals, and where because of the particular nature of your product, it's important that it has a distinctive appearance. I should warn you, however, that once you start designing your own visual design, it takes a huge amount of work to get something that looks good, original, and professional. Because the examples in this chapter are designed to illustrate the programming principles in as few pages as possible, they won't look particularly professional, but will give you an idea of what can in principle be done.

If you want any form or control to be non-rectangular, the way to do this is to set to its Control.Region property to indicate the region within which you want the form to be displayed. For example, if you want a form that is shaped as a downward-pointing equilateral triangle of width 200, you could use this code inside the Form constructor:

 GraphicsPath outline = new GraphicsPath(); outline.AddLine(0, 0, 200, 0); outline.AddLine(200, 0, 100, 174); outline.AddLine(100, 174, 0, 0); Region rgn = new Region(outline); this. Region = rgn; 

Although this is simple in principle, there are a couple of complicating factors that you need to be aware of. Setting a region will automatically prevent the form from displaying as an XP-themed form, so you'll be back to the W2K/9x title bar, unless of course you take control of the drawing of that area - customizing your UI really is an all-or-nothing thing. When you define the region that you want the form to be confined to, Windows simply sets up a clipper that makes sure that no drawing can take place outside of this region. It also intercepts mouse events so that they are only sent to this form if the mouse is located within the region. However, for all other purposes, Windows still regards the form as occupying the full original rectangle. This means that all drawing and measuring operations take place with coordinates relative to the top left corner of the original rectangle - and Windows does not make any attempt to adjust any drawing to take account of your region. Anything that would have been displayed outside of the region you've defined simply gets clipped. That includes any text in any controls, as well as borders, title bar, caption, and the close, minimize and maximize buttons on a form. This can have unfortunate consequences: users are unlikely to appreciate a form that doesn't have a title bar - especially when they discover that there's no way to visually tell if the form has the focus, and nowhere on the form that they can click to move, resize, minimize or maximize or close it! So if you are going to create non-rectangular forms, you do need to be very careful how you go about it. You really have two options: make sure your region includes all the important areas of the form, or write your own code to implement your own title bar and other items somewhere that is visible. The latter option is of course a huge task - something to be undertaken only if it is really important for the form to be that particular shape.

You should also be aware that the Visual Studio .NET Design View draws controls occupying their default rectangular areas, and does not take account of non-rectangular regions.

Circular Form Example

We'll now present an example in which we develop a form with a semi-circular shape, as shown:

click to expand

The form has two buttons on the left, one of which populates the list box in the center of the form with names of the colors of the rainbow, and the other with the names of 2D shapes. I've specified that not only the main form, but also the two buttons, have a half-elliptical shape. The list box is its normal rectangular shape for now. The ultimate aim is for the list box to be the same shape as the form (the lower half of an ellipse), but that would mean much of the text in the list box would not be displayed since it would lie outside the ellipse. We can solve that with problem an owner-draw list box - and we'll do that in the next section when we examine owner-draw controls. But for now we'll leave the list box as a rectangle.

The form is of course resizable, and the if the form is resized, the buttons will automatically reposition themselves down the left hand curve of the form's border, while the list box will remain central - this means that you can freely resize the form and it'll still keep its unusual, groovy-looking appearance.

The screenshot already illustrates some of the potential problems associated with non-rectangular windows. It is clear that the edges of both the form and the buttons look visibly untidy. We could solve this by manually drawing a border around the curved edge of the form and by setting up the buttons as owner draw buttons, but since doing so involves a fair amount of work and does not demonstrate any new principles, we'll leave the example as it is.

The example is a standard Windows Forms VS.NET project, with the buttons and list box added as shown. The controls are respectively named btnColors, btnShapes, and lbResults. I've also changed the font size in the list box to a larger, bolder font as shown in the screenshot, and set the form's MinimumSize property to (350, 250) - any smaller size would make ruin the appearance of the form, since for example the buttons would overlap the list box.

We need to add the following member fields to the form:

 public class Form1 : System.Windows.Forms.Form {    private const int nButtons = 2;    private Button [] buttons;    private string [] colors = { "red", "orange", "yellow", "green",                                 "blue", "indigo", "violet" };    private string [] shapes = { "square", "circle", "triangle",                                 "hexagon", "pentagon" };    private System.Windows.Forms.Button btnColors;    private System.Windows.Forms.Button btnShapes;    private System.Windows.Forms.ListBox lbResults; 

The extra variables added are the string arrays that contain the text to be added to the list box. buttons is an array that will hold the two Button references - holding these in an array will simplify the code to manipulate them. The array is initialized in the constructor:

 public Form1() {    InitializeComponent();    buttons = new Button[nButtons];    buttons[0] = btnColors;    buttons[1] = btnShapes;    SetButtonRegions();    DoResize(); } 

SetButtonRegions() is the method that sets the shape of the buttons, while DoResize() sets the shape of the form and the location of the controls. We'll look at these methods soon. DoResize() needs to be invoked whenever the size of the form changes. The recommended place to handle updating the layout of controls on a form is in the form's Layout event handler, so we add this handler:

 private void Form1_Layout(object sender,                           System.Windows.Forms.LayoutEventArgs e) {    DoResize(); } 

DoResize() simply calls a number of other methods to shape the form, and layout the buttons and list box:

 private void DoResize() {    SetFormRegion();    SetButtonLocations();    SetListBoxLocation(); } 

The following is the code to set the shape of the buttons: the SetButtonRegion() method. Note that this method does not need to be called from the Layout event handler since the size and shape of the buttons don't change after form startup - only the locations. This method is therefore invoked only from the Form1 constructor.

 private void SetButtonRegions() {    int width = this.buttons[0].Width;    int height = this.buttons[0].Height;    GraphicsPath outline = new GraphicsPath();    Rectangle twiceButtonRect = new Rectangle(-width, 0, 2 * width, height);    outline.AddArc(twiceButtonRect, -90, 180);    outline.AddLine(0, height, 0, 0);    Region rgn = new Region(outline);    foreach (Button button in this.buttons)       button.Region = rgn;  } 

This code instantiates a System.Drawing.Drawing2D.GraphicsPath object, which will be used to define the region. The half-ellipse is added to the path using the GraphicsPath.AddArc() method. Note that this method needs to be supplied with a Rectangle that defines the size that the full ellipse would have if it were drawn in full. Since the right half of the ellipse occupies the full button rectangle, the Rectangle we supply here needs to be twice that size, stretching out to the left of the button. All coordinates in the regions are given relative to the top-left corner of each button, which means the same Region can be used for both buttons. The AddLine() call closes the GraphicsPath, so we end up with a path that looks like this:

click to expand

In the diagram, the thick line indicates the required graphics path, the thin line the border of the button.

The code to set up the region for the form is more complex, because the region is more complex in shape. We don't want it to simply be a semi-ellipse because we don't want any of the title bar to be cut out of the region. Instead, we set up a region that consists of a rectangle covering the title bar, and a half-ellipse below it:

click to expand

The code to set up this region looks like this:

 private void SetFormRegion() {    int titleBarHeight = this.ClientTopLeft.Y;    int remainingHeight = this.Height - titleBarHeight;    GraphicsPath outline = new GraphicsPath();    outline.AddLine(0, titleBarHeight, 0, 0) ;    outline.AddLine(0, 0, this.Width,0);    outline.AddLine(Width, 0, Width, titleBarHeight);    // twiceClientRect covers area below title bar and equal    // area above it, to set bounds for ellipse    Rectangle twiceClientRect = new Rectangle(0, titleBarHeight -                      remainingHeight, this.Width, 2 * remainingHeight);    outline.AddArc(twiceClientRect, 0, 180);    Region rgn = new Region(outline);    this.Region = rgn; } 

This code makes use of a small utility property that works out the location of the top-left corner of the client area of the screen relative to the top-left corner of the form - the y-coordinate of this relative offset gives the title bar height.

 public Point ClientTopLeft {    get    {       Point pt = PointToScreen(new Point(0, 0));       return new Point(pt.X - this.Location.X, pt.Y - this.Location.Y);    } } 

Next, the code that sets the list box location and size - this code is called from the DoResize() method, and hence invoked at construction time and whenever the form is resized. The list box is to be located 1/3 across the form horizontally, and has height 2/3 the client area height of the form:

 private void SetListBoxLocation() {    this.lbResults.Location = new Point(this.Width / 3, 5);    this.lbResults.Size = new Size(this.Width / 3,                                   (this.ClientSize.Height * 2) / 3); } 

Setting the button location is more complex since the location depends on the curve of the left-hand size of the form. The following utility method works out how many pixels across from the left side of the form's rectangular area the actual border is at a given number of pixels from the top:

 private int LeftBorderY2X(int y) {    int titleBarHeight = this.ClientTopLeft.Y;    int remainingHeight = this.Height - titleBarHeight;    double yOverH = ((double)y) / ((double)remainingHeight);    double sqrt = Math.Sqrt(1.0 - yOverH * yOverH);    return (int) ((1.0 - sqrt) * ((double)this.Width) / 2.0); } 

Don't worry too much about the math. It's basically using Pythagoras' theorem. Now that we have this utility method, we can position the buttons:

 private void SetButtonLocations() {    for (int i=0; i<nButtons; i++)    {       int y = 5 + (int) ((double) (this.buttons[0].Height * i) * 1.7);       int x = LeftBorderY2X(y + this.buttons[i].Height);       this.buttons[i].Location = new Point(x, y);    } } 

This code places the first button five pixels below the title bar, then separates the buttons by 70% of their height (we assume all buttons are the same size - I made sure of that in the Design view). Each button is inset so that its bottom-left corner just touches the curved border of the form.

That deals with all the code needed to lay out the controls. Back onto more routine Windows Forms stuff, we also need to supply event handlers for the buttons:

 private void btnShapes_Click(object sender, System.EventArgs e) {    lbResults.Items.Clear();    lbResults.Items.AddRange(this.shapes); } private void btnColors_Click(object sender, System.EventArgs e) {    lbResults.Items.Clear();    lbResults.Items.AddRange(this.colors); } 

And that completes the application.



Advanced  .NET Programming
Advanced .NET Programming
ISBN: 1861006292
EAN: 2147483647
Year: 2002
Pages: 124

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net