问题
Background:
I have a requirement to create a dimming effect on another monitor. I think I solved it by using a WPF Window that takes up the entire screen dimensions with Topmost
and AllowsTransparency
= True. It has an inner black glow effect and has the style WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW
applied to it (among other things) to allow users to click through to the apps behind it.
I monitor for EVENT_OBJECT_REORDER
events in Windows and call SetWindowPos
to force the Topmost state above other Topmost windows. It seems to work well so far in my proof of concept testing.
The problem I found was this dimming (window) would cover the task bar, but not if I click the Start Menu. I'm currently testing with Windows 10. If I click the Start Menu, it causes the Start Menu and Taskbar to appear above the dimming (window). I wanted everything to remain dim, always.
I solved this issue by setting uiAccess
=true in the app manifest, generating a self-signed cert, and copying the exe over to "c:\program files*". This allows me to force a Topmost state for my window, even above the Start Menu.
My questions:
Is there a way to position a window over the Start Menu without
uiAccess
? Or even another way to force dimness to a screen without using a window (but not dependent on monitor drivers or hardware capabilities)?If not, what considerations do I need to keep in mind when distributing a WPF app (via a WiX setup project or something similar) that is to bypass UIPI restrictions with
uiAccess
=True? Can I simply install my self signed cert during the setup process? Will the user run into any additional hurdles? Will I, as a developer, run into any additional hurdles while building this (aside from what I've already mentioned)?
Thank you!
回答1:
I monitor for EVENT_OBJECT_REORDER events
You are using SetWinEventHook(). This scenario fails the classic "what if two programs do this" bracket. Raymond Chen discussed this pretty well in this blog post, giving your approach a dedicated post.
This is a lot more common than you might assume. Every Windows machine has a program that does this for example, run Osk.exe
, the on-screen keyboard program. Interesting experiment, I predict it will flicker badly for a while but assume it will eventually give up. Not actually sure it does, last time I tried this was at Vista time and it wouldn't, please let us know.
Fairly sure you will conclude that this isn't the right way to go about it so uiAccess is moot as well. You needed it here to bypass UIPI and make SetWindowPos() work. An aspect of UAC that blocks attempts by a program to hijack an elevated program's capabilities. Covering the Start window qualifies as a DOS attack. Bigger problem here is that your self-signed certificate isn't going to work, you'll have to buy a real one. Sets you back several hundred dollars every ~7 years.
Controlling monitor brightness with software isn't that easy to do correctly. Everybody reaches for SetDeviceGammaRamp() and that is what you should do as well. The MSDN docs will give you plenty of FUD but afaik every mainstream video adapter driver implements it. It was popular in games. One unavoidable limitation is that it is only active for the desktop in which your program runs. So not for the secure desktop (screen saver and Ctrl+Alt+Del) and not for other login sessions unless they start your program as well.
WMI is too flaky to consider. Not so sure why it fails so often, I assume it has something to do with the often less-than-stellar I2C interconnect between the video adapter and the monitor. Or laptops that want to control brightness with an Fn keystroke, that feature always wins. Or the Windows feature that automatically adjusts brightness based on ambient light, invariably the more desirable way to do this and a hard act to follow.
Most common outcome is likely to be a shrug at your program and a curse of the user at the clumsy monitor controls. But he'll fiddle with it and figure it out. Sorry.
回答2:
This won't answer anything about uiAccess=true
, but...
Dimming the Screen
As an alternative way to dim the screen, you could try using SetDeviceGammaRamp to dim all screens at once (if that's desired).
For example, take the following helper class:
/// <summary> Allows changing the gamma of the displays. </summary>
public static class GammaChanger
{
/// <summary>
/// Retrieves the current gamma ramp data so that it can be restored later.
/// </summary>
/// <param name="gamma"> [out] The current gamma. </param>
/// <returns> true if it succeeds, false if it fails. </returns>
public static bool GetCurrentGamma(out GammaRampRgbData gamma)
{
gamma = GammaRampRgbData.Create();
return GetDeviceGammaRamp(GetDC(IntPtr.Zero), ref gamma);
}
public static bool SetGamma(ref GammaRampRgbData gamma)
{
// Now set the value.
return SetDeviceGammaRamp(GetDC(IntPtr.Zero), ref gamma);
}
public static bool SetBrightness(int gamma)
{
GammaRampRgbData data = new GammaRampRgbData
{
Red = new ushort[256],
Green = new ushort[256],
Blue = new ushort[256]
};
int wBrightness = gamma; // reduce the brightness
for (int ik = 0; ik < 256; ik++)
{
int iArrayValue = ik * (wBrightness + 128);
if (iArrayValue > 0xffff)
{
iArrayValue = 0xffff;
}
data.Red[ik] = (ushort)iArrayValue;
data.Green[ik] = (ushort)iArrayValue;
data.Blue[ik] = (ushort)iArrayValue;
}
return SetGamma(ref data);
}
[DllImport("gdi32.dll")]
private static extern bool SetDeviceGammaRamp(IntPtr hdc, ref GammaRampRgbData gammaRgbArray);
[DllImport("gdi32.dll")]
private static extern bool GetDeviceGammaRamp(IntPtr hdc, ref GammaRampRgbData gammaRgbArray);
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct GammaRampRgbData
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
public UInt16[] Red;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
public UInt16[] Green;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
public UInt16[] Blue;
/// <summary> Creates a new, initialized GammaRampRgbData object. </summary>
/// <returns> A GammaRampRgbData. </returns>
public static GammaRampRgbData Create()
{
return new GammaRampRgbData
{
Red = new ushort[256],
Green = new ushort[256],
Blue = new ushort[256]
};
}
}
}
Combined with the following in a static void Main()
, and the program will change the brightness until the user exits the application:
GammaChanger.GammaRampRgbData originalGamma;
bool success = GammaChanger.GetCurrentGamma(out originalGamma);
Console.WriteLine($"Originally: {success}");
success = GammaChanger.SetBrightness(44);
Console.WriteLine($"Setting: {success}");
Console.ReadLine();
success = GammaChanger.SetGamma(ref originalGamma);
Console.WriteLine($"Restoring: {success}");
Console.ReadLine();
Do note however, that this is applying a global solution to a local problem
If you do go this route, I'd suggest really making sure that you're restoring the user's gamma before exiting, otherwise they'll be left with a less than steller experience that your app crashed and the screen is no permanently dimmed.
Sources:
- Discussion of the usage of SetGammaRamp, which is where a bulk of the algorithm comes from
- An alternative implementation of the above solution.
来源:https://stackoverflow.com/questions/35671903/considerations-when-installing-a-desktop-wpf-app-with-uiaccess-true