Welcome to Pete Brown's 10rem.net

First time here? If you are a developer or are interested in Microsoft tools and technology, please consider subscribing to the latest posts.

You may also be interested in my blog archives, the articles section, or some of my lab projects such as the C64 emulator written in Silverlight.

(hide this)

Creating Big Silverlight Windows and Getting Monitor Resolutions and Positions with PInvoke

Pete Brown - 07 February 2012

While doing the (long!) tech review for Silverlight 5 in Action, my friend and former coworker Tom McKearney mentioned that we should put together some code to make handling windows across multiple monitors a reasonable task in Silverlight 5. Then, on a mailing list, I recently saw the question:

Hi all -- I'm wondering if there is a maximum window size for Silverlight documented anywhere. For example if I had hardware that could support a 3x3 or 3x4 grid of 1920x1080 displays, can I make an SL5 OOB window that big? Can I make individual items in the window that big? Can I get hardware acceleration on all of it? Does the 3D API have any different limits than XAML? Or maybe Silverlight has no specific limit, but it's up to the hardware?

Curious about that, I decided to try it myself, and combine this with what Tom had requested. I don't have a 3x3 array of screens that size, however. It really is a serious number of pixels.

image

Cost of screens aside, most people don't have the video hardware to be able to run 6 displays. ATI has some displayport cards which will do that, but otherwise you're looking at a minimum of three video cards and a variety of connections. You're also looking at a shedload of video memory.

What I do have is a pair of 30" displays each running at 2560x1600. That's quite a bit smaller, but still much larger than most people have. It ends up being a bit less than half the requested pixels. I do get almost the right width, but the height is lacking.

image

I wouldn't want 3x3 of the HD res screens (too much bevel) unless they were big old TV-style ones and you were doing relatively low-res graphics. 3x3 of the 2560x1600 though? *drool*

In any case, my two combined are still quite a bit larger than the typical HD res 1920x1080 screen people have, with its 2,073,600 pixels, and is a valid test of creating big windows in Silverlight.

My displays are also oriented a little differently than you may expect:

image

That means that logical 0,0 is in the middle of the combined display. We'll need to know that for later.

Creating the Window

If you want to learn how to create out-of-browser windows in Silverlight 5, see my post here.

The first test is to see if I can create a window that fits the dimensions of my two screens. All sizes here will be hard-coded.

XAML

<UserControl x:Class="PeteBrown.SilverlightBigWindow.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="175
" d:DesignWidth="800">

<Grid x:Name="LayoutRoot" Background="White">
<ListBox x:Name="DisplayList"
Margin="126,12,12,12">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="50" />
<ColumnDefinition Width="150" />
<ColumnDefinition Width="150" />
<ColumnDefinition Width="75" />
<ColumnDefinition Width="75" />
</Grid.ColumnDefinitions>

<TextBlock Grid.Column="0"
Text="{Binding MonitorName}" />
<TextBlock Grid.Column="1"
Text="{Binding IsPrimary}" />
<TextBlock Grid.Column="2"
Text="{Binding MonitorArea}" />
<TextBlock Grid.Column="3"
Text="{Binding WorkArea}" />
<TextBlock Grid.Column="4"
Text="{Binding Width}" />
<TextBlock Grid.Column="5"
Text="{Binding Height}" />


</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>


<Button Content="Open Full"
Height="23"
HorizontalAlignment="Left"
Margin="12,12,0,0"
Name="OpenWindow"
VerticalAlignment="Top"
Width="108"
Click="OpenWindow_Click" />
<Button Content="Open Primary"
Height="23"
HorizontalAlignment="Left"
Margin="12,41,0,0"
Name="OpenPrimary"
VerticalAlignment="Top"
Width="108"
Click="OpenPrimary_Click" />
<Button Content="Open Secondary"
Height="23"
HorizontalAlignment="Left"
Margin="12,70,0,0"
Name="OpenSecondary"
VerticalAlignment="Top"
Width="108"
Click="OpenSecondary_Click" />
</Grid>
</UserControl>

C# code

private void OpenWindow_Click(object sender, RoutedEventArgs e)
{
Window w = new Window();

w.Width = 5120;
w.Height = 1600;

w.Show();
}

private void OpenPrimary_Click(object sender, RoutedEventArgs e)
{
}

private void OpenSecondary_Click(object sender, RoutedEventArgs e)
{
}

This will allow me to click a button and have it open a giant window on the screen. The window has no content, so it'll appear as a plain white empty window.

Run it (remember, must be elevated trust out-of-browser app) and what do you see? No problem. One giant white window which fits across my screen.

image

The problem is, however, that it starts in the middle of my displays instead of over at the far left. I had to drag it to the far left to get it to take up the whole set of displays. How do we figure out how to position and size the window?

Positioning and Sizing the Window

On my setup, the monitor to the left is all in negative space. So, to move the window to the far left, I need to move it to -2560. Then we can open the window and then tell it to move to the top left, or to take up just a single display.

private void OpenWindow_Click(object sender, RoutedEventArgs e)
{
Window w = new Window();

w.Width = 5120;
w.Height = 1600;

w.Left = -2560;
w.Top = 0;
w.Show();
}

private void OpenPrimary_Click(object sender, RoutedEventArgs e)
{
Window w = new Window();

w.Width = 2560;
w.Height = 1600;


w.Left = 0;
w.Top = 0;
w.Show();
}

private void OpenSecondary_Click(object sender, RoutedEventArgs e)
{
Window w = new Window();

w.Width = 2560;
w.Height = 1600;

w.WindowStyle = WindowStyle.None;

w.WindowState = WindowState.Maximized;

w.Left = -2560;
w.Top = 0;
w.Show();
}

Ok, so we've verified that it can be done. Now we need to make it work for arbitrary resolutions and monitor configurations. Before we do that, though, did you notice that the sizing was off a bit? Both the width and height appear to be off by the measurements of the window chrome. It turns out this happens whether or not you're using custom chrome. I've reported a bug to the team and they're investigating, so for now we're going to have to ignore the size issues. If you work around the sizing in your own code, stick it in an conditional compilation block or something so you can easily change it if/when the bug is fixed.

Getting Display Informatin Using PInvoke

We're running in elevated OOB mode anyway, so as long as you only care about Windows, you can also throw in some P/Invoke. The main functions we're interested in are EnumDisplayMonitors and GetMonitorInfo.

The PInvoke.net example was enough to get me started, but it lacked a few necessary steps. Of course, it also needed some changes to work with Silverlight.

The first addition is the DisplayInfo class. We'll use this to hold information about a single monitor.

namespace PeteBrown.SilverlightBigWindow
{
public class DisplayInfo
{
public string MonitorName { get; internal set; }
public Win32Rect MonitorArea { get; internal set; }
public Win32Rect WorkArea { get; internal set; }
public int Width { get; internal set; }
public int Height { get; internal set; }
public bool IsPrimary { get; internal set; }
}
}

Next, I need a way to populate this class. Here's where all the PInvoke action happens.

using System;
using System.Runtime.InteropServices;
using System.Collections.ObjectModel;

namespace PeteBrown.SilverlightBigWindow
{

[StructLayout(LayoutKind.Sequential)]
public struct Win32Rect
{
public int Left { get; set; }
public int Top { get; set; }
public int Right { get; set; }
public int Bottom { get; set; }

public override string ToString()
{
return string.Format("{0}, {1}, {2}, {3}", Left, Top, Right, Bottom);
}
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal struct MonitorInfoEx
{
public int Size;
public Win32Rect Monitor;
public Win32Rect WorkArea;
public uint Flags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = DisplayManager.DeviceNameCharacterCount)]
public string DeviceName;

public void Init()
{
this.Size = 40 + 2 * DisplayManager.DeviceNameCharacterCount;
this.DeviceName = string.Empty;
}
}


public class DisplayManager
{
// size of a device name string
internal const int DeviceNameCharacterCount = 32;


private delegate bool MonitorEnumProcDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref Win32Rect lprcMonitor, uint dwData);

[DllImport("user32.dll")]
private static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumProcDelegate lpfnEnum, uint dwData);

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfoEx lpmi);

private static ObservableCollection<DisplayInfo> _displays = new ObservableCollection<DisplayInfo>();
public static ObservableCollection<DisplayInfo> Displays
{
get { return _displays; }
}


public static void LoadDisplays()
{
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, MonitorEnumProc, 0);
}

[AllowReversePInvokeCalls]
internal static bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref Win32Rect lprcMonitor, uint dwData)
{
var monitor = new MonitorInfoEx();
monitor.Init();

bool success = GetMonitorInfo(hMonitor, ref monitor);

if (success)
{
var display = new DisplayInfo();

display.MonitorName = monitor.DeviceName;

display.Width = monitor.Monitor.Right - monitor.Monitor.Left;
display.Height = monitor.Monitor.Bottom - monitor.Monitor.Top;

display.MonitorArea = monitor.Monitor;
display.WorkArea = monitor.WorkArea;
display.IsPrimary = (monitor.Flags > 0);

_displays.Add(display);
}

return true;
}
}
}

This code calls EnumDisplayMonitors to enumerate the monitors on the system. For each monitor found, I then call GetMonitorInfo to pull back the details. From that call, I create a DisplayInfo class and populate it, ready to be used by the rest of the Silverlight application. The list of DisplayInfo classes is stored in a class-scope collection.

Note the "AllowReversePInvokeCalls" attribute on MonitorEnumProc. That attribute is required for any callbacks. It also means we couldn't use a simple anonymous delegate for the callback, as those can't have attributes.

Finally, a short call in MainPage.xaml and we're able to get the display sizes

public MainPage()
{
InitializeComponent();

DisplayManager.LoadDisplays();

DisplayList.ItemsSource = DisplayManager.Displays;
}

Run the application. On my screen, it looks like this:

image

Ok, so now we know how to position a display using hard-coded values, and how to get the display list from windows. Now, to combine the two to help with sizing and positioning.

Positioning and Sizing the Window

Positioning the window can be tricky. There are any number of ways that monitors can be configured. Here are just a few interesting (and common) ones, in addition to the ones mentioned above:

image

If you look at the teal and the green examples, you can see where a rectangular pixel mapping puts some logical pixels out into never-never land. On systems like that, a single window spanning all displays is not particularly practical. In any case, most multi-display configurations are two, or at most, three displays because that's all you can typically drive from a single typical video card.

As an aside, I've run with all four examples shown above, plus a few extras. My current layout is 2x 30" displays side by side with primary on right and secondary on left, plus a 23" display on top of the primary. The 23" display is connected to a different computer, however, and shared using Input Director, so it doesn't factor into this discussion.

With just the two displays I have, it's easy to test a number of different scenarios simply by changing resolution and monitor position in display settings. First, some code to position the window on a specific screen.

Position Window on Primary Display

A PC can have only one primary display. Conveniently, the display is called out as such via the API.

private void PositionWindowOnSingleScreen(DisplayInfo screen, Window window, bool SizeToScreen = true, bool maximize = false)
{
window.Left = screen.MonitorArea.Left;
window.Top = screen.MonitorArea.Top;

if (SizeToScreen)
{
window.Width = screen.Width;
window.Height = screen.Height;
}

if (maximize)
window.WindowState = WindowState.Maximized;
}


private void OpenPrimary_Click(object sender, RoutedEventArgs e)
{
if (DisplayManager.Displays.Count > 0)
{
// find primary display
DisplayInfo info = (from DisplayInfo di in DisplayManager.Displays
where di.IsPrimary
select di).FirstOrDefault();

if (info != null)
{
Window w = new Window();

PositionWindowOnSingleScreen(info, w, true, false);

w.Show();
}
else
{
MessageBox.Show("No primary display. I suppose you won't see this message.");
}
}
else
{
MessageBox.Show("Display list not yet loaded.");
}

}

This example uses a little LINQ to find the primary screen and then a separate function to position the window on that screen. I can't think of a case where you'd have no primary display, but I check for it anyway.

Position Window on Secondary Display

Primary is cool. You can do that without any API calls. Secondary is normally a little more work, but I have you covered.

You can, of course, have more than one secondary display. In my case, I have only one, so I'm going to position this window in the first secondary display I have.

private void OpenSecondary_Click(object sender, RoutedEventArgs e)
{
if (DisplayManager.Displays.Count > 0)
{
// find first secondary display
DisplayInfo info = (from DisplayInfo di in DisplayManager.Displays
where !di.IsPrimary
select di).FirstOrDefault();

if (info != null)
{
Window w = new Window();

PositionWindowOnSingleScreen(info, w, true, false);

w.Show();
}
else
{
MessageBox.Show("No secondary display available.");
}
}
else
{
MessageBox.Show("Display list not yet loaded.");
}
}

In this case, all I needed to change was the error message and add a little bang in the where clause of the LINQ query.

Make a window take up all Display Space

This goes specifically to the 3x3 screen scenario. As I mentioned, we'll need to assume rectangular window space here, as anything else is going to be pretty application-specific.

private void OpenWindow_Click(object sender, RoutedEventArgs e)
{
if (DisplayManager.Displays.Count > 0)
{
Window w = new Window();

w.Left = (from DisplayInfo di in DisplayManager.Displays
select di.MonitorArea.Left).Min();

w.Top = (from DisplayInfo di in DisplayManager.Displays
select di.MonitorArea.Top).Min();

w.Width = (from DisplayInfo di in DisplayManager.Displays
select di.Width).Sum();

w.Height = (from DisplayInfo di in DisplayManager.Displays
select di.Height).Sum();

w.Show();
}
else
{
MessageBox.Show("Display list not yet loaded.");
}
}

Because we're assuming a reasonably logical rectangle, I can simply take the minimum left and top values and use that to position the window, and then sum the height and width values in order to size the window.

As an aside, Silverlight had no issues opening this window on my machine. I'm not sure I'd recommend stretching a video across it, though.

Well, actually, I can't let that lie out there like that. Let's try it. Add this code right after the w.Height setting and before the w.Show() line

MediaElement video = new MediaElement();
video.Width = w.Width;
video.Height = w.Height;
video.Source = new Uri("/pub/sl5iA/NetduinoRobot_SmallM.wmv", UriKind.Absolute);
video.Stretch = Stretch.UniformToFill;
video.AutoPlay = true;

w.Content = video;

I stretch the video so it takes up all space (UniformToFill) and clips the video so the aspect ratio stays correct. It works, and works well, If the the video was high enough resolution, it would even look good.

image

Sweet! Big video. That's almost like IMax or UHDTV :)

Position Main Window on a Named Display

This is particularly useful if you're restoring windows when starting up a new session. You could store the display device name and then do some checks on startup and if the device name and resolution are still valid (they could have swapped displays, bought new ones, got rid of one, etc.) position the window on that display. If not, position it on the main display.

In this case, I'm simply going to position the main Silverlight window on to the display you click on in the ListBox. The code is simple enough. First, add a button to the MainPage, under the other buttons.

<Button Content="Move to Selected"
Height="23"
HorizontalAlignment="Left"
Margin="12,115,0,0"
Name="MoveToSelectedButton"
VerticalAlignment="Top"
Width="108"
Click="MoveToSelectedButton_Click" />

Next, the event handler and work function in the code-behind. There are many ways I could have done this (especially using SelectedItem), but I specifically want to show using the device name. I know it's extra work, but I'm doing it on-purpose; the value you store upon exiting is not going to be an object instance, it's going to be a screen device name string.

private void MoveMainWindowToSelectedDisplay()
{
// see comments in blog post as to why I went with strings
string deviceName = ((DisplayInfo)DisplayList.SelectedItem).MonitorName;

DisplayInfo display = (from DisplayInfo di in DisplayManager.Displays
where di.MonitorName.ToLower() == deviceName.ToLower()
select di).FirstOrDefault();

var mainWindow = Application.Current.MainWindow;

// center on the display

mainWindow.Left = display.MonitorArea.Left + (display.Width - mainWindow.Width) / 2;
mainWindow.Top = display.MonitorArea.Top + (display.Height - mainWindow.Height) / 2;

}

private void MoveToSelectedButton_Click(object sender, RoutedEventArgs e)
{
if (DisplayList.SelectedItem != null)
{
MoveMainWindowToSelectedDisplay();
}
else
{
MessageBox.Show("Please select a screen from the list");
}
}

The code moves the window to the selected monitor and then centers in that monitor. Some additional checking to make sure the window isn't wider than that monitor would be a good idea.

Summary

The intent of this post was to give you the foundation you'll need in order to do window manipulation in Silverlight. Once you start doing multi-monitor window manipulation, you probably have a good idea of what you want to do with it. I'll leave it up to you to take this code and run with it to suit your specific application needs.

     

Source Code and Related Media

Download /media/83451/petebrown.silverlightbigwindow.zip
posted by Pete Brown on Tuesday, February 7, 2012
filed under:      

7 comments for “Creating Big Silverlight Windows and Getting Monitor Resolutions and Positions with PInvoke”

  1. Tom McKearneysays:
    Very cool.

    I wanted to highlight that big snapshot of the video and add a Comment that said "unlike all those TV ads, this image is NOT simulated!", but realized I wasn't reviewing your book :)

    Thanks for the linkage as well.

    T
  2. virtualCableTVsays:
    Thanks for putting your skills to this task Pete but your references to desktp displays and such implies you may have missed the subtle point that spanning across multiple screens has become in demand for those wanting to deploy a line of business to digital signage and connected TV "video walls."
  3. Petesays:
    @virtualCableTV

    Perhaps I didn't convey it well, but I certainly didn't miss out on that point. I was talking about what people can reasonably test and experiment with on their own systems.

    I've actually had discussions with a number of people doing digital signage in WPF and Silverlight.

    Pete
  4. MawashiKidsays:
    This is an excellent article. The best I've seen on the topic so far.
    I have done some tests in OOB + Isolated Storage
    and I found P/Invoke methods used Displaymanager.cs code pretty useful to gather all the monitor informations I needed.
    The exemple you give is pretty concise and straightforward, I don't know how I could have done it in an easier way than this.

    Re: Positioning the window can be tricky. There are any number of ways that monitors can be configured...

    Indeed, even with 3 monitors we can end up with many combination scenarios...
    [3 monitors in a row with primary in the middle, 2 small monitors on the top - with one main primary monitor on the bottom...]
    so I 'd say I always prefer having a good grid graphic mockup of screen resolution to start with when testing...
    Other than that, I imagine each monitors infos could still be gathered randomly from DisplayManager
    but would require a bit more coding afterwards when it comes to positionning...

    Thank's for sharing.
  5. Alsays:
    Hi Pete, excellent post!
    You mentioned "This is particularly useful if you're restoring windows when starting up a new session"
    How would I go about restoring the "resolution" caused by AutoZoom via Ctrl-MouseWheel on a Silvelight OOB application?

    I would like to restore for example 50% zoomed-out (Application.Current.Host.Content.ZoomFactor set to 0.5)

    Initially, I tried something like the following in the Loaded event:

    // Zoom out 50%
    ScaleTransform scale = new ScaleTransform();
    scale.ScaleX = 0.5;
    scale.ScaleY = 0.5;
    this.RenderTransform = scale;
    //Application.Current.RootVisual.RenderTransform = scale;
    //Application.Current.MainWindow.Content.RenderTransform = scale;

    However, this only causes the application to shrink to the upper-left quadrant OK and leaving white space in the 3 others quadrants. That’s not the effect I get when I interactively zoom out with control-MouseWheel where
    1) the layout continues to occupy the full window (maximized/no border at construction).
    2) for example, my vertical list shows *more* items now sized down.

    That’s the desired effect, I would like to restore to upon restart of the application.
    Thank you.
  6. Alsays:
    Hi Pete, excellent post!
    You mentioned "This is particularly useful if you're restoring windows when starting up a new session"
    How would I go about restoring the "resolution" caused by AutoZoom via Ctrl-MouseWheel on a Silvelight OOB application?

    I would like to restore for example 50% zoomed-out (Application.Current.Host.Content.ZoomFactor set to 0.5)

    Initially, I tried something like the following in the Loaded event:

    // Zoom out 50%
    ScaleTransform scale = new ScaleTransform();
    scale.ScaleX = 0.5;
    scale.ScaleY = 0.5;
    this.RenderTransform = scale;
    //Application.Current.RootVisual.RenderTransform = scale;
    //Application.Current.MainWindow.Content.RenderTransform = scale;

    However, this only causes the application to shrink to the upper-left quadrant OK and leaving white space in the 3 others quadrants. That’s not the effect I get when I interactively zoom out with control-MouseWheel where
    1) the layout continues to occupy the full window (maximized/no border at construction).
    2) for example, my vertical list shows *more* items now sized down.

    That’s the desired effect, I would like to restore to upon restart of the application.
    Thank you.

Comment on this Post

Remember me