Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Change colour filter of screen to work with multiple monitors

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?

like image 247
Dan W Avatar asked Apr 21 '15 21:04

Dan W


2 Answers

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++));
like image 141
Alex Avatar answered Sep 21 '22 14:09

Alex


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.

like image 21
Hans Passant Avatar answered Sep 19 '22 14:09

Hans Passant