/**************************************************************************\ Copyright Microsoft Corporation. All Rights Reserved. \**************************************************************************/ #pragma warning disable 1591, 618 using AntdWpf.Microsoft.Windows.Shell.Standard; namespace Microsoft.Windows.Shell { using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Security; using System.Windows; using System.Windows.Media; using Standard; [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")] internal class SystemParameters2 : INotifyPropertyChanged { private delegate void _SystemMetricUpdate(IntPtr wParam, IntPtr lParam); [ThreadStatic] private static SystemParameters2 _threadLocalSingleton; private MessageWindow _messageHwnd; private bool _isGlassEnabled; private Color _glassColor; private SolidColorBrush _glassColorBrush; private Thickness _windowResizeBorderThickness; private Thickness _windowNonClientFrameThickness; private double _captionHeight; private Size _smallIconSize; private string _uxThemeName; private string _uxThemeColor; private bool _isHighContrast; private CornerRadius _windowCornerRadius; private Rect _captionButtonLocation; private readonly Dictionary> _UpdateTable; #region Initialization and Update Methods // Most properties exposed here have a way of being queried directly // and a way of being notified of updates via a window message. // This region is a grouping of both, for each of the exposed properties. private void _InitializeIsGlassEnabled() { IsGlassEnabled = NativeMethods.DwmIsCompositionEnabled(); } private void _UpdateIsGlassEnabled(IntPtr wParam, IntPtr lParam) { // Neither the wParam or lParam are used in this case. _InitializeIsGlassEnabled(); } private void _InitializeGlassColor() { bool isOpaque; uint color; NativeMethods.DwmGetColorizationColor(out color, out isOpaque); color |= isOpaque ? 0xFF000000 : 0; WindowGlassColor = Utility.ColorFromArgbDword(color); var glassBrush = new SolidColorBrush(WindowGlassColor); glassBrush.Freeze(); WindowGlassBrush = glassBrush; } private void _UpdateGlassColor(IntPtr wParam, IntPtr lParam) { bool isOpaque = lParam != IntPtr.Zero; uint color = unchecked((uint)(int)wParam.ToInt64()); color |= isOpaque ? 0xFF000000 : 0; WindowGlassColor = Utility.ColorFromArgbDword(color); var glassBrush = new SolidColorBrush(WindowGlassColor); glassBrush.Freeze(); WindowGlassBrush = glassBrush; } private void _InitializeCaptionHeight() { Point ptCaption = new Point(0, NativeMethods.GetSystemMetrics(SM.CYCAPTION)); WindowCaptionHeight = DpiHelper.DevicePixelsToLogical(ptCaption, SystemParameters2.DpiX / 96.0, SystemParameters2.Dpi / 96.0).Y; } private void _UpdateCaptionHeight(IntPtr wParam, IntPtr lParam) { _InitializeCaptionHeight(); } private void _InitializeWindowResizeBorderThickness() { Size frameSize = new Size( NativeMethods.GetSystemMetrics(SM.CXSIZEFRAME), NativeMethods.GetSystemMetrics(SM.CYSIZEFRAME)); Size frameSizeInDips = DpiHelper.DeviceSizeToLogical(frameSize, SystemParameters2.DpiX / 96.0, SystemParameters2.Dpi / 96.0); WindowResizeBorderThickness = new Thickness(frameSizeInDips.Width, frameSizeInDips.Height, frameSizeInDips.Width, frameSizeInDips.Height); } private void _UpdateWindowResizeBorderThickness(IntPtr wParam, IntPtr lParam) { _InitializeWindowResizeBorderThickness(); } private void _InitializeWindowNonClientFrameThickness() { Size frameSize = new Size( NativeMethods.GetSystemMetrics(SM.CXSIZEFRAME), NativeMethods.GetSystemMetrics(SM.CYSIZEFRAME)); Size frameSizeInDips = DpiHelper.DeviceSizeToLogical(frameSize, SystemParameters2.DpiX / 96.0, SystemParameters2.Dpi / 96.0); int captionHeight = NativeMethods.GetSystemMetrics(SM.CYCAPTION); double captionHeightInDips = DpiHelper.DevicePixelsToLogical(new Point(0, captionHeight), SystemParameters2.DpiX / 96.0, SystemParameters2.Dpi / 96.0).Y; WindowNonClientFrameThickness = new Thickness(frameSizeInDips.Width, frameSizeInDips.Height + captionHeightInDips, frameSizeInDips.Width, frameSizeInDips.Height); } private void _UpdateWindowNonClientFrameThickness(IntPtr wParam, IntPtr lParam) { _InitializeWindowNonClientFrameThickness(); } private void _InitializeSmallIconSize() { SmallIconSize = new Size( NativeMethods.GetSystemMetrics(SM.CXSMICON), NativeMethods.GetSystemMetrics(SM.CYSMICON)); } private void _UpdateSmallIconSize(IntPtr wParam, IntPtr lParam) { _InitializeSmallIconSize(); } private void _LegacyInitializeCaptionButtonLocation() { // This calculation isn't quite right, but it's pretty close. // I expect this is good enough for the scenarios where this is expected to be used. int captionX = NativeMethods.GetSystemMetrics(SM.CXSIZE); int captionY = NativeMethods.GetSystemMetrics(SM.CYSIZE); int frameX = NativeMethods.GetSystemMetrics(SM.CXSIZEFRAME) + NativeMethods.GetSystemMetrics(SM.CXEDGE); int frameY = NativeMethods.GetSystemMetrics(SM.CYSIZEFRAME) + NativeMethods.GetSystemMetrics(SM.CYEDGE); Rect captionRect = new Rect(0, 0, captionX * 3, captionY); captionRect.Offset(-frameX - captionRect.Width, frameY); WindowCaptionButtonsLocation = captionRect; } [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] private void _InitializeCaptionButtonLocation() { // There is a completely different way to do this on XP. if (!Utility.IsOSVistaOrNewer || !NativeMethods.IsThemeActive()) { _LegacyInitializeCaptionButtonLocation(); return; } var tbix = new TITLEBARINFOEX { cbSize = Marshal.SizeOf(typeof(TITLEBARINFOEX)) }; IntPtr lParam = Marshal.AllocHGlobal(tbix.cbSize); try { Marshal.StructureToPtr(tbix, lParam, false); // This might flash a window in the taskbar while being calculated. // WM_GETTITLEBARINFOEX doesn't work correctly unless the window is visible while processing. // use SW.SHOWNA instead SW.SHOW to avoid some brief flashing when launched the window NativeMethods.ShowWindow(_messageHwnd.Handle, SW.SHOWNA); NativeMethods.SendMessage(_messageHwnd.Handle, WM.GETTITLEBARINFOEX, IntPtr.Zero, lParam); tbix = (TITLEBARINFOEX)Marshal.PtrToStructure(lParam, typeof(TITLEBARINFOEX)); } finally { NativeMethods.ShowWindow(_messageHwnd.Handle, SW.HIDE); Utility.SafeFreeHGlobal(ref lParam); } // TITLEBARINFOEX has information relative to the screen. We need to convert the containing rect // to instead be relative to the top-right corner of the window. RECT rcAllCaptionButtons = RECT.Union(tbix.rgrect_CloseButton, tbix.rgrect_MinimizeButton); // For all known themes, the RECT for the maximize box shouldn't add anything to the union of the minimize and close boxes. Assert.AreEqual(rcAllCaptionButtons, RECT.Union(rcAllCaptionButtons, tbix.rgrect_MaximizeButton)); RECT rcWindow = NativeMethods.GetWindowRect(_messageHwnd.Handle); // Reorient the Top/Right to be relative to the top right edge of the Window. var deviceCaptionLocation = new Rect( rcAllCaptionButtons.Left - rcWindow.Width - rcWindow.Left, rcAllCaptionButtons.Top - rcWindow.Top, rcAllCaptionButtons.Width, rcAllCaptionButtons.Height); Rect logicalCaptionLocation = DpiHelper.DeviceRectToLogical(deviceCaptionLocation, SystemParameters2.DpiX / 96.0, SystemParameters2.Dpi / 96.0); WindowCaptionButtonsLocation = logicalCaptionLocation; } private void _UpdateCaptionButtonLocation(IntPtr wParam, IntPtr lParam) { _InitializeCaptionButtonLocation(); } private void _InitializeHighContrast() { HIGHCONTRAST hc = NativeMethods.SystemParameterInfo_GetHIGHCONTRAST(); HighContrast = (hc.dwFlags & HCF.HIGHCONTRASTON) != 0; } private void _UpdateHighContrast(IntPtr wParam, IntPtr lParam) { _InitializeHighContrast(); } private void _InitializeThemeInfo() { if (!NativeMethods.IsThemeActive()) { UxThemeName = "Classic"; UxThemeColor = ""; return; } try { // wrap GetCurrentThemeName in a try/catch as we were seeing an exception // even though the theme service seemed to be active (UxTheme IsThemeActive) // see http://stackoverflow.com/questions/8893854/wpf-application-ribbon-crash as an example. string name; string color; string size; NativeMethods.GetCurrentThemeName(out name, out color, out size); // Consider whether this is the most useful way to expose this... UxThemeName = System.IO.Path.GetFileNameWithoutExtension(name); UxThemeColor = color; } catch (Exception) { UxThemeName = "Classic"; UxThemeColor = ""; } } private void _UpdateThemeInfo(IntPtr wParam, IntPtr lParam) { _InitializeThemeInfo(); } private void _InitializeWindowCornerRadius() { // The radius of window corners isn't exposed as a true system parameter. // It instead is a logical size that we're approximating based on the current theme. // There aren't any known variations based on theme color. Assert.IsNeitherNullNorEmpty(UxThemeName); // These radii are approximate. The way WPF does rounding is different than how // rounded-rectangle HRGNs are created, which is also different than the actual // round corners on themed Windows. For now we're not exposing anything to // mitigate the differences. var cornerRadius = default(CornerRadius); // This list is known to be incomplete and very much not future-proof. // On XP there are at least a couple of shipped themes that this won't catch, // "Zune" and "Royale", but WPF doesn't know about these either. // If a new theme was to replace Aero, then this will fall back on "classic" behaviors. // This isn't ideal, but it's not the end of the world. WPF will generally have problems anyways. switch (UxThemeName.ToUpperInvariant()) { case "LUNA": cornerRadius = new CornerRadius(6, 6, 0, 0); break; case "AERO": // Aero has two cases. One with glass and one without... if (NativeMethods.DwmIsCompositionEnabled()) { cornerRadius = new CornerRadius(8); } else { cornerRadius = new CornerRadius(6, 6, 0, 0); } break; case "CLASSIC": case "ZUNE": case "ROYALE": default: cornerRadius = new CornerRadius(0); break; } WindowCornerRadius = cornerRadius; } private void _UpdateWindowCornerRadius(IntPtr wParam, IntPtr lParam) { // Neither the wParam or lParam are used in this case. _InitializeWindowCornerRadius(); } #endregion /// /// Private constructor. The public way to access this class is through the static Current property. /// private SystemParameters2() { // This window gets used for calculations about standard caption button locations // so it has WS_OVERLAPPEDWINDOW as a style to give it normal caption buttons. // This window may be shown during calculations of caption bar information, so create it at a location that's likely offscreen. _messageHwnd = new MessageWindow((CS)0, WS.OVERLAPPEDWINDOW | WS.DISABLED, (WS_EX)0, new Rect(-16000, -16000, 100, 100), "", _WndProc); _messageHwnd.Dispatcher.ShutdownStarted += (sender, e) => Utility.SafeDispose(ref _messageHwnd); // Fixup the default values of the DPs. _InitializeIsGlassEnabled(); _InitializeGlassColor(); _InitializeCaptionHeight(); _InitializeWindowNonClientFrameThickness(); _InitializeWindowResizeBorderThickness(); _InitializeCaptionButtonLocation(); _InitializeSmallIconSize(); _InitializeHighContrast(); _InitializeThemeInfo(); // WindowCornerRadius isn't exposed by true system parameters, so it requires the theme to be initialized first. _InitializeWindowCornerRadius(); _UpdateTable = new Dictionary> { { WM.THEMECHANGED, new List<_SystemMetricUpdate> { _UpdateThemeInfo, _UpdateHighContrast, _UpdateWindowCornerRadius, _UpdateCaptionButtonLocation, } }, { WM.SETTINGCHANGE, new List<_SystemMetricUpdate> { _UpdateCaptionHeight, _UpdateWindowResizeBorderThickness, _UpdateSmallIconSize, _UpdateHighContrast, _UpdateWindowNonClientFrameThickness, _UpdateCaptionButtonLocation, } }, { WM.DWMNCRENDERINGCHANGED, new List<_SystemMetricUpdate> { _UpdateIsGlassEnabled } }, { WM.DWMCOMPOSITIONCHANGED, new List<_SystemMetricUpdate> { _UpdateIsGlassEnabled } }, { WM.DWMCOLORIZATIONCOLORCHANGED, new List<_SystemMetricUpdate> { _UpdateGlassColor } }, }; } public static SystemParameters2 Current { get { if (_threadLocalSingleton == null) { _threadLocalSingleton = new SystemParameters2(); } return _threadLocalSingleton; } } private IntPtr _WndProc(IntPtr hwnd, WM msg, IntPtr wParam, IntPtr lParam) { // Don't do this if called within the SystemParameters2 constructor if (_UpdateTable != null) { List<_SystemMetricUpdate> handlers; if (_UpdateTable.TryGetValue(msg, out handlers)) { Assert.IsNotNull(handlers); foreach (var handler in handlers) { handler(wParam, lParam); } } } return NativeMethods.DefWindowProc(hwnd, msg, wParam, lParam); } public bool IsGlassEnabled { get { // return _isGlassEnabled; // It turns out there may be some lag between someone asking this // and the window getting updated. It's not too expensive, just always do the check. return NativeMethods.DwmIsCompositionEnabled(); } private set { if (value != _isGlassEnabled) { _isGlassEnabled = value; _NotifyPropertyChanged("IsGlassEnabled"); } } } public Color WindowGlassColor { get { return _glassColor; } private set { if (value != _glassColor) { _glassColor = value; _NotifyPropertyChanged("WindowGlassColor"); } } } public SolidColorBrush WindowGlassBrush { get { return _glassColorBrush; } private set { Assert.IsNotNull(value); Assert.IsTrue(value.IsFrozen); if (_glassColorBrush == null || value.Color != _glassColorBrush.Color) { _glassColorBrush = value; _NotifyPropertyChanged("WindowGlassBrush"); } } } public Thickness WindowResizeBorderThickness { get { return _windowResizeBorderThickness; } private set { if (value != _windowResizeBorderThickness) { _windowResizeBorderThickness = value; _NotifyPropertyChanged("WindowResizeBorderThickness"); } } } public Thickness WindowNonClientFrameThickness { get { return _windowNonClientFrameThickness; } private set { if (value != _windowNonClientFrameThickness) { _windowNonClientFrameThickness = value; _NotifyPropertyChanged("WindowNonClientFrameThickness"); } } } public double WindowCaptionHeight { get { return _captionHeight; } private set { if (value != _captionHeight) { _captionHeight = value; _NotifyPropertyChanged("WindowCaptionHeight"); } } } public Size SmallIconSize { get { return new Size(_smallIconSize.Width, _smallIconSize.Height); } private set { if (value != _smallIconSize) { _smallIconSize = value; _NotifyPropertyChanged("SmallIconSize"); } } } [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Ux")] [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ux")] public string UxThemeName { get { return _uxThemeName; } private set { if (value != _uxThemeName) { _uxThemeName = value; _NotifyPropertyChanged("UxThemeName"); } } } [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Ux")] [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ux")] public string UxThemeColor { get { return _uxThemeColor; } private set { if (value != _uxThemeColor) { _uxThemeColor = value; _NotifyPropertyChanged("UxThemeColor"); } } } public bool HighContrast { get { return _isHighContrast; } private set { if (value != _isHighContrast) { _isHighContrast = value; _NotifyPropertyChanged("HighContrast"); } } } public CornerRadius WindowCornerRadius { get { return _windowCornerRadius; } private set { if (value != _windowCornerRadius) { _windowCornerRadius = value; _NotifyPropertyChanged("WindowCornerRadius"); } } } public Rect WindowCaptionButtonsLocation { get { return _captionButtonLocation; } private set { if (value != _captionButtonLocation) { _captionButtonLocation = value; _NotifyPropertyChanged("WindowCaptionButtonsLocation"); } } } #region Per monitor dpi support private enum CacheSlot : int { DpiX, NumSlots } private static int _dpi; private static bool _dpiInitialized; private static readonly object _dpiLock = new object(); private static bool _setDpiX = true; private static BitArray _cacheValid = new BitArray((int)CacheSlot.NumSlots); private static int _dpiX; internal static int Dpi { [SecurityCritical, SecurityTreatAsSafe] get { if (!_dpiInitialized) { lock (_dpiLock) { if (!_dpiInitialized) { using (var dc = SafeDC.GetDesktop()) { if (dc.DangerousGetHandle() == IntPtr.Zero) { throw new Win32Exception(); } _dpi = NativeMethods.GetDeviceCaps(dc, DeviceCap.LOGPIXELSY); _dpiInitialized = true; } } } } return _dpi; } } /// /// Critical as this accesses Native methods. /// TreatAsSafe - it would be ok to expose this information - DPI in partial trust /// internal static int DpiX { [SecurityCritical, SecurityTreatAsSafe] get { if (_setDpiX) { lock (_cacheValid) { if (_setDpiX) { _setDpiX = false; // Win32Exception will get the Win32 error code so we don't have to #pragma warning disable 6523 using (var dc = SafeDC.GetDesktop()) { // Detecting error case from unmanaged call, required by PREsharp to throw a Win32Exception #pragma warning disable 6503 if (dc.DangerousGetHandle() == IntPtr.Zero) { throw new Win32Exception(); } #pragma warning restore 6503 #pragma warning restore 6523 _dpiX = NativeMethods.GetDeviceCaps(dc, DeviceCap.LOGPIXELSX); _cacheValid[(int)CacheSlot.DpiX] = true; } } } } return _dpiX; } } #endregion Per monitor dpi support #region INotifyPropertyChanged Members private void _NotifyPropertyChanged(string propertyName) { Assert.IsNeitherNullNorEmpty(propertyName); var handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } public event PropertyChangedEventHandler PropertyChanged; #endregion } }