2010年2月23日 星期二

DPI Virtualization

這是一個在Vista之後才出現的系統設定
目的是要讓一些原本不是DPI aware的應用程式
可以達到如同DPI aware(WPF視窗都屬於此類)一樣的效果
根據微軟的說法,是希望在各種屬性的平台上面都可以有舒適的效果
請參考Writing High-DPI Win32 Applications - DPI Virtualization

舉例來說,在Win7同樣是1920x1080的解析度之下
同樣透過GetSystemMetrics這個API去讀取解析度
DPI 設定WPF視窗WinForm視窗Win32 Console
100% (96)1920x10801920x10801920x1080
125% (120)1920x10801920x10801920x1080
150% (144)1920x10801280x7201280x720

在DPI=144(150%)的時候,非DPI aware的程式會將解析度自動調整
參考在WPF中取得螢幕的解析度
就發現在WPF程式中使用SystemParameters會得到一樣的結果
這是因為預設在150%以上的時候,DPI Virtualization會被啟動
如果點選自訂DPI設定,就會發現Use Windows XP Style DPI Scaling沒有被勾選了(在100%或是125%則是預設被勾選的)

如果只是在WPF中或是純粹在WinForm底下做處理倒是沒關係
但是有時候會有透過舊有的程式取得資訊的時候
這時候如果不知道DPI Virtualization的設定
就可能出現一些非預期的結果
(請在Win7 150%的設定下VS2008按滑鼠右鍵就知道我在說什麼)

我在MSDN上面提問,但是卻沒有回應,應該是沒有提供這樣的API
不過因為系統設定基本上都會寫在註冊表中
所以我就直接對在同樣設定下,diff有沒有勾選該選項的註冊表
發現其中有一個值是會跟著改變的

[HKEY_CURRENT_USER\Software\Microsoft\Windows\DWM]
"UseDpiScaling"=dword:00000000

所以透過讀取註冊表的API就可以判斷是否有DPI Virtualization的設定

bool IsDpiVirtualization()
{
/// open the key of Desktop Window Manager
RegistryKey k = Registry.CurrentUser.OpenSubKey(
"Software\\Microsoft\\Windows\\DWM");

if (k != null)
{
/// get the value if it exists
if ((int)k.GetValue("UseDpiScaling", 0) == 1)
{
k.Close();
return true;
}
k.Close();
}
return false;
}

--
參考資料
Writing High-DPI Win32 Applications
Current Word - 「XP Style DPI Scaling」
API to get "Use Windows XP Style DPI Scaling" setting
RegistryKey.OpenSubKey 方法
Registry.GetValue 方法

在WPF中取得DPI的設定

要取得DPI的設定,需要一個針對一個視窗
在WPF中可以透過主視窗來取得形變的比率
再乘上預設的DPI(96)即可

void GetDpiSetting(
out double DpiX, out double DpiY)
{
const double DEFAULT_DPI = 96.0;

/// get transform matrix from current main window
Matrix m = PresentationSource
.FromVisual(Application.Current.MainWindow)
.CompositionTarget.TransformToDevice;

/// scale default dpi
DpiX = m.M11 * DEFAULT_DPI;
DpiY = m.M22 * DEFAULT_DPI;
}

另外習慣使用Win32的程式設計師
一樣可以使用GetDeviceCaps取得DPI的設定

const int LOGPIXELSX = 88;
const int LOGPIXELSY = 90;

[DllImport("gdi32.dll")]
static extern int GetDeviceCaps(IntPtr hdc, int Index);

[DllImport("user32.dll")]
static extern IntPtr GetDC(IntPtr Hwnd);

void GetDpiSetting(
out double DpiX, out double DpiY)
{
/// get desktop dc
IntPtr h = GetDC(IntPtr.Zero);

/// get dpi from dc
DpiX = GetDeviceCaps(h, LOGPIXELSX);
DpiY = GetDeviceCaps(h, LOGPIXELSY);
}

--
參考資料
Getting system DPI in WPF app
GetDeviceCaps Function

在WPF中取得螢幕的解析度

在WPF中有一個很簡單可以取得系統設定的類別
所以可以直接透過SystemParameters取得主螢幕的解析度

Size GetScreenResolution()
{
return new Size(
SystemParameters.PrimaryScreenWidth,
SystemParameters.PrimaryScreenHeight);
}

當然,如果是習慣使用Win32的程式設計師
也一樣可以用GetSystemMetrics來取得

const int SM_CXSCREEN = 0;
const int SM_CYSCREEN = 1;

[DllImport("user32.dll")]
static extern int GetSystemMetrics(int nIndex);

Size GetScreenResolution()
{
return new Size(
GetSystemMetrics(SM_CXSCREEN),
GetSystemMetrics(SM_CYSCREEN));
}

此時要注意DPI的設定,在非預設的DPI(96)下
由於WPF的特性,這兩種方法取出的數值會不同
例如同樣在1920x1080的解析度下
DPI 設定SystemParametersGetSystemMetrics
100% (96)1920x10801920x1080
125% (120)1536x8641920x1080
150% (144)1280x7201920x1080

這樣彷彿使用GetSystemMetrics比較一致
但在WPF中,視窗相對位置會使用SystemParameters的解析度來算
也就是一個最大化的視窗,在滑鼠事件中取得右下角座標時
會取得SystemParameters的大小
所以還是需要依照使用的需求而選用
有關DPI的設定可以參考在WPF中取得DPI的設定DPI Virtualization

--
參考資料
SystemParameters 類別
GetSystemMetrics Function

2010年2月22日 星期一

在WPF中自訂Cursor

如果要使用WPF內建的游標符號(Cursor)可以直接透過

void SetCursor()
{
Cursor = Cursors.ArrowCD;
}

內建的游標符號種類可以參考Cursors 類別
但是有時候我們會希望要把游標設成一張圖或是一個UI元件的樣子
這時候就要透過Win32的幾個函式了

struct IconInfo
{
public bool fIcon;
public int xHotspot;
public int yHotspot;
public IntPtr hbmMask;
public IntPtr hbmColor;
}

[DllImport("user32.dll")]
static extern IntPtr CreateIconIndirect(
ref IconInfo piconinfo);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetIconInfo(
IntPtr hIcon,
ref IconInfo piconinfo);

然後將一張圖檔(System.Drawing.Bitmap)轉成WPF的Cursor

Cursor CreatCursor(Bitmap b, int x, int y)
{
/// get icon from input bitmap
IconInfo ico = new IconInfo();
GetIconInfo(b.GetHicon(), ref ico);

/// set the hotspot
ico.xHotspot = x;
ico.yHotspot = y;
ico.fIcon = false;

/// create a cursor from iconinfo
IntPtr cursor = CreateIconIndirect(ref ico);
return CursorInteropHelper.Create(
new SafeFileHandle(cursor, true));
}

這裡hotspot是指真正的游標在這張圖的哪個位置
如果是(0,0),整張圖會在游標的右下方
如果是圖的寬高的一半的話,那就會像是抓到整張圖的中心在動

另外,有時候會希望要把游標設成一個UIElement的樣子
那就先做一個轉換

Cursor CreatCursor(UIElement u, Point p)
{
Cursor c;

/// move to the orignal point of parent
u.Measure(new Size(double.PositiveInfinity,
double.PositiveInfinity));
u.Arrange(new Rect(0, 0,
u.DesiredSize.Width,
u.DesiredSize.Height));

/// render the source to a bitmap image
RenderTargetBitmap r =
new RenderTargetBitmap(
(int)u.DesiredSize.Width,
(int)u.DesiredSize.Height,
96, 96, PixelFormats.Pbgra32);
r.Render(u);

/// reset back to the orignal position
u.Measure(new Size(0, 0));

using (MemoryStream m = new MemoryStream())
{
/// use an encoder to transform to Bitmap
PngBitmapEncoder e = new PngBitmapEncoder();
e.Frames.Add(BitmapFrame.Create(r));
e.Save(m);
System.Drawing.Bitmap b =
new System.Drawing.Bitmap(m);

/// create cursor from Bitmap
c = CreatCursor(b,
(int)p.X, (int)p.Y);
}

return c;
}

有關measure及arrange可以參考在WPF中的畫面重排(Measure & Arrange)在WPF中存圖檔的問題(RenderTargetBitmap)

--
參考資料
FrameworkElement.Cursor 屬性
WPF Tutorial - How To Use Custom Cursors
ICONINFO Structure
CreateIconIndirect Function
GetIconInfo Function
SafeFileHandle 類別