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.
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.
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:
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.
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:
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:
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.
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.