7 October 2011
You’ve heard me preach it before: fix those margins, align, and try and make your apps as beautiful and inspiring as you can (see also: my designer tips for developers).
On Thursday, I stopped by the Windows Phone Design Team’s beautiful studio space here on campus. It’s a really creative space, packed with fun people, chill music, and a genuine desire to see even better, more beautiful apps in the marketplace.
While sitting with two of the UX designers, Corrina and Arturo, and talking about some of these design principles, I just happened to be introduced to a nifty, albeit simple, gridline/grid that the team’s been using for some time while working on the Windows Phone. It’s just a set of red squares, 25x25 pixels, offset 12 pixels from one another, and all contained within a page padding of 24px. (Again: the magical Metro number is 12-based on those edges)
The design typically will use a Photoshop layer containing these squares, or maybe some XAML inserted on top of a page in an app, to work on alignment, visualizing the grid, etc., and making tweaks.
I got to thinking: it would be nice if this could be just like the performance counters that we have for the Windows Phone: at debug time, you could opt into overlaying this grid on top of the frame of the entire app, being omnipresent. I coded up a quick implementation during that meeting and here it is!
To use the counter, simply open up the App.xaml.cs file (where the other performance counters are) and add this in. I’d recommend just enabling it when you want to do a quick design pass. You can use this then both with apps on your phone as well as the emulator; it’s helpful in the emulator since you can then screenshot the UI and share that with your friends and family who can critique you on your pixel placement.
// Show graphics profiling information while debugging. if (System.Diagnostics.Debugger.IsAttached) { // Display the current frame rate counters. Application.Current.Host.Settings.EnableFrameRateCounter = true; // Display the metro grid helper. MetroGridHelper.IsVisible = true;
Here’s what it looks like at runtime in a very simple app:
In this example, I have one of the classic Windows Phone design “bugs”: one of the TextBlocks has just been inserted, without using the appropriate standard phone style. As a result, it has a Margin value of “0” instead of being 12 pixels offset from the left. It’s more clear now with the squares, since you can see the misalignment easily.
I’ve also added simple static properties for Opacity and Color if you’d rather not use the Red default color and ~0.15 opacity on the squares. The IsVisible property can be flipped off at runtime, but beware that the grid is still present in the visual tree once you add it – so there will be a performance hit (don’t ship an app that uses the grid in release builds).
I’ve published the source file on NuGet – that’s a really easy way to get the file into your project, and if I make any fixes or add features in the future, it’ll be easy for you to get those changes:
Or you can drop this source code into a new file of your project, MetroGridHelper.cs:
// (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System.Collections.Generic; using System.Diagnostics; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; namespace System.Windows { /// <summary> /// A utility class that overlays a designer-friendly grid on top of the /// application frame, for use similar to the performance counters in /// App.xaml.cs. The color and opacity are configurable. The grid contains /// a number of squares that are 24x24, offset with 12px gutters, and all /// 24px away from the edge of the device. /// </summary> public static class MetroGridHelper { private static bool _visible; private static double _opacity = 0.15; private static Color _color = Colors.Red; private static List<Rectangle> _squares; private static Grid _grid; /// <summary> /// Gets or sets a value indicating whether the designer grid is /// visible on top of the application's frame. /// </summary> public static bool IsVisible { get { return _visible; } set { _visible = value; UpdateGrid(); } } /// <summary> /// Gets or sets the color to use for the grid's squares. /// </summary> public static Color Color { get { return _color; } set { _color = value; UpdateGrid(); } } /// <summary> /// Gets or sets a value indicating the opacity for the grid's squares. /// </summary> public static double Opacity { get { return _opacity; } set { _opacity = value; UpdateGrid(); } } /// <summary> /// Updates the grid (if it already has been created) or initializes it /// otherwise. /// </summary> private static void UpdateGrid() { if (_squares != null) { var brush = new SolidColorBrush(_color); foreach (var square in _squares) { square.Fill = brush; } if (_grid != null) { _grid.Visibility = _visible ? Visibility.Visible : Visibility.Collapsed; _grid.Opacity = _opacity; } } else { BuildGrid(); } } /// <summary> /// Builds the grid. /// </summary> private static void BuildGrid() { _squares = new List<Rectangle>(); var frame = Application.Current.RootVisual as Frame; if (frame == null || VisualTreeHelper.GetChildrenCount(frame) == 0) { Deployment.Current.Dispatcher.BeginInvoke(BuildGrid); return; } var child = VisualTreeHelper.GetChild(frame, 0); var childAsBorder = child as Border; var childAsGrid = child as Grid; if (childAsBorder != null) { // Not a pretty way to control the root visual, but I did not // want to implement using a popup. var content = childAsBorder.Child; if (content == null) { Deployment.Current.Dispatcher.BeginInvoke(BuildGrid); return; } childAsBorder.Child = null; Deployment.Current.Dispatcher.BeginInvoke(() => { Grid newGrid = new Grid(); childAsBorder.Child = newGrid; newGrid.Children.Add(content); PrepareGrid(frame, newGrid); }); } else if (childAsGrid != null) { PrepareGrid(frame, childAsGrid); } else { Debug.WriteLine("Dear developer:"); Debug.WriteLine("Unfortunately the design overlay feature requires that the root frame visual"); Debug.WriteLine("be a Border or a Grid. So the overlay grid just isn't going to happen."); return; } } /// <summary> /// Does the actual work of preparing the grid once the parent frame is /// in the visual tree and we have a Grid instance to work with for /// placing the chilren. /// </summary> /// <param name="frame">The phone application frame.</param> /// <param name="parent">The parent grid to insert the sub-grid into.</param> private static void PrepareGrid(Frame frame, Grid parent) { var brush = new SolidColorBrush(_color); _grid = new Grid(); _grid.IsHitTestVisible = false; // To support both orientations, unfortunately more visuals need to // be used. An alternate implementation would be to react to the // orientation change event and re-draw/remove squares. double width = frame.ActualWidth; double height = frame.ActualHeight; double max = Math.Max(width, height); for (int x = 24; x < /*width*/ max; x += 37) { for (int y = 24; y < /*height*/ max; y += 37) { var rect = new Rectangle { Width = 25, Height = 25, VerticalAlignment = System.Windows.VerticalAlignment.Top, HorizontalAlignment = System.Windows.HorizontalAlignment.Left, Margin = new Thickness(x, y, 0, 0), IsHitTestVisible = false, Fill = brush, }; _grid.Children.Add(rect); _squares.Add(rect); } } _grid.Visibility = _visible ? Visibility.Visible : Visibility.Collapsed; _grid.Opacity = _opacity; // For performance reasons a single surface should ideally be used // for the grid. _grid.CacheMode = new BitmapCache(); // Places the grid into the visual tree. It is never removed once // being added. parent.Children.Add(_grid); } } }
Hope this helps.
Jeff Wilcox is a Software Engineer at Microsoft in the Open Source Programs Office (OSPO), helping Microsoft engineers use, contribute to and release open source at scale.