Change colour filter of screen to work with multiple monitors
Asked Answered
S

2

6

I've made a program to change the colour filter of the screen similar to the way Flux does (the code shown to do this is in the main question from here). However, a couple of my users say it won't affect the other screen/s with two or more monitors. How would I modify the code so that it does?

Sleuthhound answered 21/4, 2015 at 21:48 Comment(2)
Does it do nothing on a machine with two monitors or does it throw an error? Does it only change a single screen? What is the user experience?Landreth
It does nothing with the second screen apparently.Sleuthhound
E
4

You could do that by

  1. getting a hold of all connected monitors
  2. applying your get / set functions to the Graphics (or its Hdc).
  3. registering with the MonitorInfoInvalidated event to re-apply, if the monitor info gets invalidated.

If you already have a dependency on the Windows.Forms dll, or don't mind taking on this dependency, you can use its Screen class for this as @HansPassant pointed out in his answer. In this case you would register an eventhandler for the SystemEvents.DisplaySettingsChanged to trigger re-applying your get/set functions, and you would use interop calls to CreateDC and DeleteDC to get/release a device context handle (IntPtr) from the Screen.DeviceName property. The below code shows a wrapper around this class that helps with doing that:

/// <summary>
/// This is an alternative that uses the Windows.Forms Screen class.
/// </summary>
public static class FormsScreens
{
    public static void ForAllScreens(Action<Screen, IntPtr> actionWithHdc)
    {
        foreach (var screen in Screen.AllScreens)
            screen.WithHdc(actionWithHdc);

    }

    public static void WithHdc(this Screen screen, Action<Screen, IntPtr> action)
    {
        var hdc = IntPtr.Zero;
        try
        {
            hdc = CreateDC(null, screen.DeviceName, null, IntPtr.Zero);
            action(screen, hdc);
        }
        finally
        {
            if (!IntPtr.Zero.Equals(hdc))
                DeleteDC(hdc);
        }
    }

    private const string GDI32 = @"gdi32.dll";

    [DllImport(GDI32, EntryPoint = "CreateDC", CharSet = CharSet.Auto)]
    static extern IntPtr CreateDC(string lpszDriver, string lpszDevice, string lpszOutput, IntPtr lpInitData);

    [DllImport(GDI32, CharSet = CharSet.Auto)]
    private static extern bool DeleteDC([In] IntPtr hdc);
}

If you don't want to take on a new dependency on the Windows.Forms dll, the ConnectedMonitors class below provides the same kind of functionality:

/// <summary>
/// This is the version that is not dependent on Windows.Forms dll.
/// </summary>
public static class ConnectedMonitors
{
    private static readonly bool _isSingleMonitor = GetSystemMetrics(SM_CMONITORS) == 0;
    private static Lazy<List<MonitorInfo>> _monitors = new Lazy<List<MonitorInfo>>(GetMonitors, true);

    public static event Action MonitorInfoInvalidated;

    public class MonitorInfo
    {
        public readonly IntPtr MonitorHandle;
        public readonly IntPtr DeviceContextHandle;
        public readonly string DeviceName;
        public readonly bool IsPrimary;
        public readonly Rectangle Bounds;
        public readonly Rectangle WorkArea;

        public void WithMonitorHdc(Action<MonitorInfo, IntPtr> action)
        {
            var hdc = DeviceContextHandle;
            var shouldDeleteDC = IntPtr.Zero.Equals(hdc);
            try
            {
                if (shouldDeleteDC)
                    hdc = CreateDC(null, DeviceName, null, IntPtr.Zero);
                action(this, hdc);
            }
            finally
            {
                if (shouldDeleteDC && !IntPtr.Zero.Equals(hdc))
                    DeleteDC(hdc);
            }
        }

        internal MonitorInfo(
            IntPtr hMonitor, 
            IntPtr hDeviceContext, 
            string deviceName,
            bool isPrimary,
            Rectangle bounds,
            Rectangle workArea)
        {
            this.MonitorHandle = hMonitor;
            this.DeviceContextHandle = hDeviceContext;
            this.DeviceName = deviceName;
            this.IsPrimary = isPrimary;
            this.Bounds = bounds;
            this.WorkArea = workArea;
        }
    }

    public static void CaptureScreen(MonitorInfo mi, string fileName)
    {
        CaptureScreen(mi).Save(fileName);
    }

    public static Bitmap CaptureScreen(MonitorInfo mi)
    {
        Bitmap screenBmp = default(Bitmap);
        mi.WithMonitorHdc((m, hdc) =>
        {
            screenBmp = new Bitmap(m.Bounds.Width, m.Bounds.Height, PixelFormat.Format32bppArgb);
            using (var destGraphics = Graphics.FromImage(screenBmp))
            {
                var monitorDC = new HandleRef(null, hdc);
                var destDC = new HandleRef(null, destGraphics.GetHdc());
                var result = BitBlt(destDC, 0, 0, m.Bounds.Width, m.Bounds.Height, monitorDC, 0, 0, unchecked((int)BITBLT_SRCCOPY));
                if (result == 0)
                    throw new Win32Exception();
            }
        });
        return screenBmp;
    }

    public static IEnumerable<MonitorInfo> Monitors
    {
        get { return _monitors.Value; }
    }

    private static List<MonitorInfo> GetMonitors()
    {
        // Get info on all monitors
        var cb = new EnumMonitorsCallback();
        EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, cb.Callback, IntPtr.Zero);

        // Register for events invalidating monitor info.
        SystemEvents.DisplaySettingsChanging += OnDisplaySettingsChanging;
        SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;

        // Return result.
        return cb.Monitors;
    }

    private class EnumMonitorsCallback
    {
        public List<MonitorInfo> Monitors = new List<MonitorInfo>();

        public bool Callback(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr lparam)
        {
            // Get its info
            var info = new MONITORINFOEX();
            info.Size = Marshal.SizeOf(typeof(MONITORINFOEX));
            GetMonitorInfo(hMonitor, ref info);

            // Decode the info
            var isPrimary = (hMonitor == (IntPtr)PRIMARY_MONITOR) || ((info.Flags & MONITORINFOF_PRIMARY) != 0);
            var bounds = Rectangle.FromLTRB(info.Monitor.Left, info.Monitor.Top, info.Monitor.Right, info.Monitor.Bottom);
            var workArea = Rectangle.FromLTRB(info.WorkArea.Left, info.WorkArea.Top, info.WorkArea.Right, info.WorkArea.Bottom);
            var deviceName = info.DeviceName.TrimEnd('\0');

            // Create info for this monitor and add it.
            Monitors.Add(new MonitorInfo(hMonitor, hdcMonitor, deviceName, isPrimary, bounds, workArea));
            return true;
        }
    }

    private static void OnDisplaySettingsChanging(object sender, EventArgs e)
    {
        InvalidateInfo();
    }

    private static void OnUserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
    {
        InvalidateInfo();
    }

    private static void InvalidateInfo()
    {
        SystemEvents.DisplaySettingsChanging -= OnDisplaySettingsChanging;
        SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
        var cur = _monitors;
        _monitors = new Lazy<List<MonitorInfo>>(GetMonitors, true);
        var notifyInvalidated = MonitorInfoInvalidated;
        if (notifyInvalidated != null)
            notifyInvalidated();
    }

    #region Interop

    private const string USER32 = @"user32.dll";
    private const string GDI32 = @"gdi32.dll";
    private const int PRIMARY_MONITOR = unchecked((int)0xBAADF00D);
    private const int MONITORINFOF_PRIMARY = 0x00000001;
    private const int SM_CMONITORS = 80;
    private const int BITBLT_SRCCOPY = 0x00CC0020;
    private const int BITBLT_CAPTUREBLT = 0x40000000;
    private const int BITBLT_CAPTURE = BITBLT_SRCCOPY | BITBLT_CAPTUREBLT;

    [StructLayout(LayoutKind.Sequential)]
    private struct RECT
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    private struct MONITORINFOEX
    {
        public int Size;
        public RECT Monitor;
        public RECT WorkArea;
        public uint Flags;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
        public string DeviceName;
    }

    delegate bool EnumMonitorsDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData);

    [DllImport(USER32, CharSet=CharSet.Auto)]
    private static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, EnumMonitorsDelegate lpfnEnum, IntPtr dwData);

    [DllImport(USER32, CharSet = CharSet.Auto)]
    private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);

    [DllImport(USER32, CharSet = CharSet.Auto)]
    private static extern int GetSystemMetrics(int nIndex);

    [DllImport(GDI32, EntryPoint = "CreateDC", CharSet = CharSet.Auto)]
    static extern IntPtr CreateDC(string lpszDriver, string lpszDevice, string lpszOutput, IntPtr lpInitData);

    [DllImport(GDI32, CharSet = CharSet.Auto)]
    private static extern bool DeleteDC([In] IntPtr hdc);

    [DllImport(GDI32, CharSet = CharSet.Auto)]
    public static extern int BitBlt(HandleRef hDC, int x, int y, int nWidth, int nHeight,
                                    HandleRef hSrcDC, int xSrc, int ySrc, int dwRop);
    #endregion
}

Usage examples for your use case, using a slightly modified SetLCDbrightness method:

private bool SetLCDbrightness(IntPtr hdc, Color c)
{
    short red = c.R;
    short green = c.G;
    short blue = c.B;

    unsafe
    {
        short* gArray = stackalloc short[3 * 256];
        short* idx = gArray;
        short brightness = 0;
        for (int j = 0; j < 3; j++)
        {
            if (j == 0) brightness = red;
            if (j == 1) brightness = green;
            if (j == 2) brightness = blue;
            for (int i = 0; i < 256; i++)
            {
                int arrayVal = i * (brightness);
                if (arrayVal > 65535) arrayVal = 65535;
                *idx = (short)arrayVal;
                idx++;
            }
        }
        // For some reason, this always returns false?
        bool retVal = SetDeviceGammaRamp(hdc, gArray);
    }
    return false;
}

Called like:

// ConnectedMonitors variant
public void SetBrightness(Color c)
{
    foreach (var monitor in ConnectedMonitors.Monitors)
        monitor.WithMonitorHdc((m, hdc) => SetLCDbrightness(hdc, c));
}

// Variant using the Windows.Forms Screen class
public void SetBrightness(Color c)
{
    var setBrightness = new Action<Screen, IntPtr>((s, hdc) => SetLCDbrightness(hdc, c));
    FormsScreens.ForAllScreens(setBrightness);
}

Note that it allows some other fun stuff like taking screenshots:

var n = 0;
foreach (var m in ConnectedMonitors.Monitors)
    ConnectedMonitors.CaptureScreen(m, string.Format(@"c:\temp\screen{0}.bmp", n++));
Escobedo answered 2/5, 2015 at 1:42 Comment(16)
Wow, had no idea it would be so complex - great work. One issue when I try to compile - I get: "The type or namespace name 'Lazy' could not be found". I can't 'Resolve' that unfortunately.Sleuthhound
@DanW The class System.Lazy<T> can be found in the System namespace. Make sure you have a using System; statement. It is available since .NET 4. If you are using an older version of .NET, a simple replacement implementation should not be hard to create.Escobedo
Right, I tend to use .NET 3.5 for all my programs due to a potentially larger userbase. Would creating it be more than 5-10 lines of code for you to add? If so, I'll see if I can switch to .NET 4 for this program.Sleuthhound
@DanW you can find one here. It is over 10 lines of code, so I guess it's time for you to upgrade to a newer .NET version ;-)Escobedo
Haha, fair enough - I'll pinch that. By the way, how does your answer compare with Hans' in terms of pros and cons?Sleuthhound
Also, just to add: the variable m isn't used in your SetLCDbrightness() function. Shall we remove it?Sleuthhound
@DanW, I edited my answer to include this information. Also refer to my comment on Hans' answer. Basically it comes down to whether or not you already have/want to take on a dependency on the Windows Forms dll, and then use either the Windows Forms Screen class or the ConnectedMonitors class, using the same strategy as outlined in the 3 bullet points at the top of my answer.Escobedo
Assuming I don't mind either way about the dependency, which method do you think may be more reliable for 99%+ of systems?Sleuthhound
They do basically the same thing, so functionality wise it would not matter. However Microsoft wrote the "Screen" class, it is a part of the standard set of libraries, so I would choose that if I did not care about the dependency.Escobedo
Congrats. Hans deserved some bounty too if I could split it, though yours took the edge due to the code and more detailed explanation. I've just heard back from one of my users, and it does indeed work on their dual monitor setup! (weirdly enough, some of my users said my program worked for dual monitors before too).Sleuthhound
@DanW Agreed, we're not doing this for bounties (that just caused me to see your question), but because it was an interesting question for a topic I had done some work on before (where I needed to do some more complicated stuff with monitors). Glad it works for your users. In the curious case where it already did I am guessing the dual monitors were using the same display adapter. Did you end up using the Windows Forms Screen class?Escobedo
Nope, just the code you gave. I'd need to research a bit more before I could implement the Screen class as my knowledge on using interop is pretty basic at present.Sleuthhound
@DanW Ok, clear. If you feel your question was properly addressed by one of the answers, you may want to mark it as the "accepted" answer, so your question can be removed from the "unanswered questions" list.Escobedo
Sorry to revisit this, but one of my users (who only has a single monitor) gets this crash message when he logs off and back on again. Doesn't happen with the previous code I had, just your multi-monitor version. The program doesn't crash completely, and clicking 'continue' on the error message, it's as if nothing ever happened. The memory of the process appeared to be within normal bounds once the error message appeared apparently. Any thoughts?Sleuthhound
@DanW That's ok. I had a quick look. One class of GDI/GDI+ "OutOfMemory" errors occurs when a device context runs out of handles. Generally because an HDC is not released. I just had a quick look and could not find anything obvious except the hdc = gg.GetHdc(); call, and possibly, not doing a graphics.ReleaseDC. I created an update that you could try in this gist. It also provides an implementation based on the Windows.Forms Screen class.Escobedo
Wonderful, I got the user to try both of your new ones, and the error is gone. Feel free to update your answer to reflect the new code if you wish. By the way, for all three techniques (including the bugged one), I had to remove the true parameter from the new Lazy<List<MonitorInfo>>(GetMonitors, true); code, as the manual implementation of Lazy only takes a single parameter.Sleuthhound
C
3

Your code cannot work when the machine has multiple display adapters, not entirely uncommon. So GetDC() is not correct, you need to instead pinvoke CreateDC(), passing the name of the screen (like @"\\.\DISPLAY1") as the 1st argument, rest null. Cleanup with DeleteDC().

Use the Screen.DeviceName property to get the device name, Screen.AllScreens() to enumerate the monitors. And you probably ought to subscribe SystemEvents.DisplaySettingsChanged event to detect that the user enabled a monitor, I don't have the hardware to check this.

Cheddar answered 2/5, 2015 at 12:12 Comment(3)
Thanks. How does your answer compare to Alex's in terms of pros and cons? I'm probably not the first person who should judge the winner of the bounty to be honest. Though for me personally, I would need your answer fleshed out some more, preferably with code. Btw, I'm sure you know, but just in case it makes any difference, note that GetDC() is not used in the method to write the screen colours, only read it. Perhaps you meant GetHdc()?Sleuthhound
I don't understand why he made it so complicated, it is rather easy with the Screen class.Cheddar
@HansPassant, you are right this could be done using the Windows Forms Screens class, which does mostly the same thing as the ConnectedMonitors class in my answer. It was something I have used in a piece of software, that did not require any other dependencies on Windows Forms, and has additional functionality for figuring out whether display devices are detachable, active, what their physical size is, supported resolutions, ... If OP doesn't need to prevent a dependency on Windows Forms, a solution using its Screens class would work equally well. I will update my answer accordingly.Escobedo

© 2022 - 2024 — McMap. All rights reserved.