Mister Goodcat

Peter's home of all things life

Friday, 10/1/2010 11:58 AM
by Peter Kuhn
0 Comments

Determine screen resolution in Silverlight OOB

Friday, 10/1/2010 11:58 AM by Peter Kuhn | 0 Comments

Sometimes, achieving simple things can be really hard. For example, when you want to know the screen resolution in a Silverlight out-of-browser application. In a full .NET application, this comes down to few lines of code, because there is a Screen class in the System.Windows.Forms namespace that provides all that information:

foreach (var screen in Screen.AllScreens)
{
    Rectangle bounds = screen.Bounds;
    Debug.WriteLine(bounds.Width + "x" + bounds.Height);
}

In Silverlight however, there is no such class. How do you get the screen resolution then?

In Browser

As long as your application runs in the  browser, you can make use of the DOM bridge to execute some JavaScript code, which can evaluate the screen resolution, like this:

int width = (int)Math.Round((double)HtmlPage.Window.Eval("screen.width"));
int height = (int)Math.Round((double)HtmlPage.Window.Eval("screen.height"));

return new Tuple<int, int>(width, height);

Out of Browser

When your application however runs out of browser, there is no DOM bridge, and the above code will not work. As it turned out, getting the screen resolution is not straight forward in that case.

My first attempt was to use COM interop and WMI. That seemed logical, because there's a specific class only to get information about the attached monitors and their data (Win32_DesktopMonitor). The code for this looks like this:

using (dynamic sWbemLocator = AutomationFactory.CreateObject("WbemScripting.SWbemLocator"))
{
    sWbemLocator.Security_.ImpersonationLevel = 3;
    sWbemLocator.Security_.AuthenticationLevel = 4;
    dynamic services = sWbemLocator.ConnectServer(".", @"root\cimv2");

    string displayConfigurationQuery = @"Select * from Win32_DesktopMonitor";                
    dynamic queryResults = services.ExecQuery(displayConfigurationQuery);

    IList<DesktopMonitor> monitors = new List<DesktopMonitor>();

    foreach (var queryResult in queryResults)
    {
        Debug.WriteLine(queryResult.ScreenWidth + "x" + queryResult.ScreenHeight);
    }

    return monitors;
}

However, there are some problems with this approach:

  • This code requires elevated permissions.
  • Apparently there is a bug or at least severe problems with that approach, because WMI does not reliable return all monitors of the system. For example, in my multi-monitor setup, only the primary screen is reported. I've found various reports on the internet of other people that have this problem too. I've tried various other Win32_x classes related to screen resolution (there are some deprecated ones), but none of them worked.
  • Since you cannot determine which monitor your application is currently running on, receiving information about multiple monitors would be rather useless anyway. Combined with the just mentioned bug this could even lead to further problems, e.g. your application running on a secondary monitor, but you get and work with the (different) resolution of the primary screen.

A better solution

It turns out that there is another solution that, although a bit hacky, provides better results. Here's the details:

  1. Temporarily switch the application to full screen mode (if it's not already running in full screen).
  2. Query the actual dimensions of the application content.
  3. Switch back to windowed mode (unless the application already was running in full screen).

The benefits of this approach are that you get the correct resolution of the monitor your application currently is displayed on (e.g. it works in multiple-monitor environments at least in that it correctly returns the current monitor's resolution), and there's the possibility to use it in applications that do not run with elevated permissions (although with the limitation that you can only use the code in response to a user action there).

The switch to full screen and back also does not require a layout pass, so you won't see any flickering or similar; switching back and forth happens in the same method without giving the UI a chance to render, so there won't be visual glitches. Enough, give me the code!

public Tuple<int, int> GetMonitorResolution()
{
    var content = Application.Current.Host.Content;
    bool wasFullScreen = content.IsFullScreen;

    try
    {
        // switch to full screen if we're are not already
        if (!wasFullScreen)
        {
            content.IsFullScreen = true;
        }
                                
        if (!content.IsFullScreen)
        {
            // the switch failed.
            // this might be due to insufficient rights
            return new Tuple<int, int>(0, 0);
        }

        // get the screen size
        int width = (int)Math.Round(content.ActualWidth);
        int height = (int)Math.Round(content.ActualHeight);

        return new Tuple<int, int>(width, height);
    }
    finally
    {
        // switch back to previous state if necessary
        if (content.IsFullScreen != wasFullScreen)
        {
            content.IsFullScreen = wasFullScreen;
        }
    }
}

Not pretty, but it works. But wait, there is more!

If your application is running with elevated permissions, you can set the full screen mode even when you're not handling a user response. However, there's a small but important detail to note. The documentation of the IsFullScreen property says:

You can set this property only in response to a user-initiated action except in trusted applications. In trusted applications, you can enter full-screen mode in (for example) a Application.Startup or FrameworkElement.Loaded event handler. However, you must do so by setting the IsFullScreen property in a delegate passed to the Dispatcher.BeginInvoke method.

What does this mean? That means that although you could use the above code snippet anywhere when your application is running with elevated permissions, it won't work in certain places like a Loaded event handler, because the switch then requires a call to Dispatcher.BeginInvoke.

That is why I wrote a second version of that method that also works in these event handlers. Please note that due to the call to Dispatcher.BeginInvoke, the code is not considered as "reaction to user input" anymore, so it won't work without elevated permissions anymore.

public void GetMonitorResolutionAsync(Action<int, int> callback)
{
    if (!Application.Current.HasElevatedPermissions)
    {
        throw new NotSupportedException("This method requires elevated permissions.");
    }

    var content = Application.Current.Host.Content;
    var dispatcher = App.Current.MainWindow.Dispatcher;
            
    if (!content.IsFullScreen)
    {
        // do this asynchronously; that will also work
        // in certain event handlers like FrameworkElement.Loaded
        dispatcher.BeginInvoke(() =>
            {
                // switch to full screen
                content.IsFullScreen = true;

                if (!content.IsFullScreen)
                {
                    // something went wrong
                    callback(0, 0);
                    return;
                }

                int width = (int)Math.Round(content.ActualWidth);
                int height = (int)Math.Round(content.ActualHeight);

                // switch back to windowed mode
                content.IsFullScreen = false;

                // send the results
                callback(width, height);
            });
    }
    else
    {
        // if we're already in full screen mode, we can simply
        // return the actual width and height values
        dispatcher.BeginInvoke(() =>
            {
                int width = (int)Math.Round(content.ActualWidth);
                int height = (int)Math.Round(content.ActualHeight);
                callback(width, height);
            });
    }
}

As a general rule of thumb, you should always use the second method when you're running with elevated permissions. Here is how to use it:

ScreenHelper screen = new ScreenHelper();
screen.GetMonitorResolutionAsync((w, h) =>
    MessageBox.Show(w + "x" + h)
    );

For your convenience, I've uploaded a source code that contains the above helper class. It also implements the method that uses JavaScript.

ScreenHelper.cs (4.83 kb)

 

Pingbacks and trackbacks (1)+

Comments are closed