共用方式為


處理 DPI 問題

越來越多的裝置隨附「高解析度」螢幕。 這些螢幕通常每英吋有 200 像素 (ppi) 以上。 在這些電腦上使用應用程式需要擴大內容,以符合以該裝置正常檢視距離來檢視內容的需求。 截至 2014 年,高密度顯示器的主要目標是行動運算裝置 (平板電腦、翻蓋式筆記型電腦和手機)。

Windows 8.1 和更新版本包含數項功能,可讓這些機器與顯示器和環境搭配運作,將機器同時連接到高密度和標準密度顯示器。

  • Windows 可讓您使用 [將文字和其他項目設為更大或更小] 設定 (自 Windows XP 以來便提供),調整內容來配合裝置。

  • Windows 8.1 和更新版本會自動調整內容,讓大部分應用程式在不同像素密度的顯示器之間移動時保持一致。 當主要顯示器是高密度 (200% 縮放比例) 且次要顯示器是標準密度 (100%) 時,Windows 會自動在次要顯示器上縮小應用程式視窗內容 (應用程式每轉譯 4 個像素顯示 1 個像素)。

  • Windows 會針對顯示器的像素密度和檢視距離預設為正確的縮放比例 (Windows 7 和更新版本,OEM 可設定)。

  • Windows 可以在超過 280 ppi 的新裝置上自動將內容擴大高達 250% (截至 Windows 8.1 S14)。

    Windows 有一種處理擴大 UI 的方式,以利用增加的像素計數。 應用程式會藉由將自己宣告為「系統 DPI 感知」來選擇加入此系統。不這麼做的應用程式會由系統擴大。 在這當中整個應用程式會統一伸展像素,而可能導致「模糊」的使用者體驗。 例如:

    DPI Issues Fuzzy

    Visual Studio 選擇加入成為 DPI 縮放比例感知,因此不會「虛擬化」。

    Windows (和 Visual Studio) 利用數種 UI 技術,當中有不同方式來處理系統所設定的比例因素。 例如:

  • WPF 會以裝置獨立的方式測量控制項 (單位,而不是像素)。 WPF UI 會針對目前的 DPI 自動擴大。

  • 不論 UI 架構為何,所有文字大小都會以點表示,而且系統也會將其視為 DPI 獨立。 Win32、WinForms 和 WPF 中的文字在繪製到顯示裝置時,已經正確擴大。

  • Win32/WinForms 對話方塊和視窗具有可讓版面配置隨文字調整大小的方法 (例如,透過網格、流程和表格版面配置面板)。 這些可避免不會隨字型大小增加時縮放的硬式編碼像素位置。

  • 系統或資源根據系統計量提供的圖示 (例如,SM_CXICON和SM_CXSMICON) 都已經擴大。

舊版 Win32 (GDI、GDI+) 和 WinForms 型 UI

雖然 WPF 已經具備高 DPI 感知能力,但我們大部分的 Win32/GDI 型程式碼最初在撰寫之時並沒有考量 DPI 感知。 Windows 已提供 DPI 縮放比例 API。 Win32 問題的修正應該會在整個產品中一致地使用這些 API。 Visual Studio 提供了協助程式類別庫,以避免重複的功能,並確保產品之間的一致性。

高解析度影像

本節主要適用於擴充 Visual Studio 2013 的開發人員。 對於 Visual Studio 2015,請使用 Visual Studio 內建的影像服務。 您可能也會發現需要支援/以許多版本的 Visual Studio 為目標,因此使用 2015 中的影像服務並不可行,因為它不存在於舊版中。 則本節也適用於您。

擴大太小的影像

太小的影像可以使用一些常見的方法在 GDI 和 WPF 上進行擴大和轉譯。 受控 DPI 協助程式類別可供內部和外部 Visual Studio 整合者使用,以解決縮放圖示、點陣圖、imagestrip 和 imagelist。 Win32 型原生 C/C++協助程式可用於縮放 HICON、HBITMAP、HIMAGELIST 和 VsUI::GdiplusImage。 點陣圖的縮放通常只需要在包含協助程式庫參考之後進行單行變更。 例如:

(WinForms) DpiHelper.LogicalToDeviceUnits(ref image);

縮放 imagelist 取決於 imagelist 是在載入時間完成,還是在執行階段附加的。 如果在載入時間完成,請使用 imagelist 呼叫 LogicalToDeviceUnits(),就像處理點陣圖一樣。 當程式碼需要在撰寫 imagelist 之前載入個別點陣圖時,請務必調整 imagelist 的影像大小:

imagelist.ImageSize = DpiHelper.LogicalToDeviceUnits(imagelist.ImageSize);

在機器碼中,建立 imagelist 時可以調整維度,如下所示:

ImageList_Create(VsUI::DpiHelper::LogicalToDeviceUnitsX(16),VsUI::DpiHelper::LogicalToDeviceUnitsY(16), ILC_COLOR32|ILC_MASK, nCount, 1);

程式庫中的函式允許指定調整大小演算法。 當縮放影像以放置在 imagelist 中時,請務必指定用於透明度的背景色彩,或使用 NearestNeighbor 縮放比例 (這會在 125% 和 150% 導致失真)。

請參閱 MSDN 上的 DpiHelper 文件。

下表顯示如何以對應的 DPI 比例因素調整影像的範例。 從 Visual Studio 2013 (100%-200% DPI 縮放比例) 起,以橙色框出的影像代表我們的最佳做法:

DPI Issues Scaling

配置問題

常見的配置問題大多可以透過使 UI 中的點保持縮放比例並相對於彼此,而不是使用絕對位置 (特別是使用像素單位) 來加以避免。 例如:

  • 配置/文字位置需要調整以考慮擴大的影像。

  • 格線中的資料行必須針對擴大的文字調整寬度。

  • 元素之間的硬式編碼大小或空間也需要擴大。 只以文字維度為基礎的大小通常沒問題,因為字型會自動擴大。

    DpiHelper 類別中提供協助程式函式,以允許在 X 和 Y 軸上進行縮放:

  • LogicalToDeviceUnitsX/LogicalToDeviceUnitsY (函式允許在 X/Y 軸上縮放)

  • 間隔空間 = DpiHelper.LogicalToDeviceUnitsX (10);

  • 間隔高度= VsUI::DpiHelper::LogicalToDeviceUnitsY(5);

    有 LogicalToDeviceUnits 多載可允許縮放物件,例如 Rect、Point 和 Size。

使用 DPIHelper 程式庫/類別來縮放影像和配置

Visual Studio DPI 協助程式庫可用於原生和受控表單,並可供其他應用程式在 Visual Studio Shell 外部使用。

若要使用程式庫,請移至 Visual Studio VSSDK 擴充性範例,並複製 High-DPI_Images_Icons 範例。

在原始檔中,包含 VsUIDpiHelper.h,並呼叫 VsUI::DpiHelper 類別的靜態函式:

#include "VsUIDpiHelper.h"

int cxScaled = VsUI::DpiHelper::LogicalToDeviceUnitsX(cx);
VsUI::DpiHelper::LogicalToDeviceUnits(&hBitmap);

注意

請勿在模組層級或類別層級靜態變數中使用協助程式函式。 程式庫也會使用靜態進行執行緒同步處理,而且您可能會遇到順序初始化問題。 將這些靜態轉換成非靜態成員變數,或將它們包裝成函式 (而能在第一次存取時建構)。

若要從將在 Visual Studio 環境內執行之受控碼存取 DPI 協助程式函式:

  • 取用的專案必須參考最新版本的 Shell MPF。 例如:

    <Reference Include="Microsoft.VisualStudio.Shell.14.0.dll" />
    
  • 確定專案具有 System.Windows.FormsPresentationCorePresentationUI 的參考。

  • 在程式碼中,使用 Microsoft.VisualStudio.PlatformUI 命名空間,並呼叫 DpiHelper 類別的靜態函式。 針對支援的型別 (點、大小、矩形等),提供可傳回新縮放物件的擴充函式。 例如:

    using Microsoft.VisualStudio.PlatformUI;
    double x = DpiHelper.LogicalToDeviceUnitsX(posX);
    Point ptScaled = ptOriginal.LogicalToDeviceUnits();
    DpiHelper.LogicalToDeviceUnits(ref bitmap);
    
    

處理可縮放 UI 中的 WPF 影像模糊

在 WPF 中,WPF 會使用高品質的雙立方演算法 (預設值) 針對目前的 DPI 縮放等級自動為點陣圖調整大小,這適用於圖片或大型螢幕擷取畫面,但不適合功能表項目圖示,因為它會產生可察覺的模糊。

建議:

  • 對於標誌影像和橫幅圖文,可以使用預設 BitmapScalingMode 調整大小模式。

  • 對於功能表項目和圖解影像,當不會造成其他失真成品時,應該使用 BitmapScalingMode 以消除模糊 (200% 和 300%)。

  • 對於非 100% 的倍數 (例如 250% 或 350%) 的大型縮放等級,使用雙立方來縮放圖解影像會導致模糊、褪色的 UI。 使用 NearestNeighbor 先將影像縮放至 100% 最大的倍數 (例如 200% 或 300%),再使用雙立方進行縮放可獲得較佳的結果。 如需詳細資訊,請參閱特殊案例:針對大型 DPI 等級預先縮放 WPF 影像。

    Microsoft.VisualStudio.PlatformUI 命名空間中的 DpiHelper 類別提供可用於繫結的成員 BitmapScalingMode。 它可讓 Visual Studio Shell 根據 DPI 比例因素,以統一的方式控制產品之間的點陣圖縮放模式。

    若要在 XAML 中使用,請新增:

xmlns:vsui="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.14.0"

<Setter Property="RenderOptions.BitmapScalingMode" Value="{x:Static vs:DpiHelper.BitmapScalingMode}" />

Visual Studio Shell 已在最上層視窗和對話方塊上設定此屬性。 在 Visual Studio 中執行的 WPF 型 UI 已經繼承它。 如果設定未傳播至特定 UI 片段,則可以在 XAML/WPF UI 的根元素上設定。 發生此狀況的地方包括彈出視窗、具有 Win32 父系的元素,以及用盡處理常式 (例如 Blend) 的設計工具視窗。

某些 UI 可與系統設定的 DPI 縮放等級分開進行縮放,例如 Visual Studio 文字編輯器和 WPF 型設計工具 (WPF 桌面和 Windows 市集)。 在這些情況下,不應使用 DpiHelper.BitmapScalingMode。 為了在編輯器中修正此問題,IDE 小組建立了標題為 RenderOptions.BitmapScalingMode 的自訂屬性。 根據系統與 UI 的合併縮放等級,將該屬性值設定為 HighQuality 或 NearestNeighbor。

特殊案例:針對大型 DPI 等級預先縮放 WPF 影像

對於非 100% 倍數的超大型縮放等級 (例如 250%、350% 等等),使用雙立方來縮放圖解影像會產生模糊、褪色的 UI。 這些影像的印象與清晰的文字幾乎就像是視覺錯覺。 影像似乎更接近眼睛並且與文字相對下失焦。 您可以使用 NearestNeighbor 先將影像縮放至 100% 的最大倍數 (例如,200% 或 300%),並使用雙立方縮放至其餘部分 (額外 50%) 來改善縮放結果。

以下是結果差異的範例,其中第一個影像使用改良的雙縮放演算法 100%->200%->250% 進行縮放,而第二個影像只使用雙立方 100%->250% 進行縮放。

DPI Issues Double Scaling Example

若要讓 UI 使用此雙縮放比例,必須修改用來顯示每個 Image 元素的 XAML 標記。 下列範例示範如何使用 DpiHelper 程式庫和 Shell.12/14 在 Visual Studio 中使用 WPF 中的雙縮放比例。

步驟 1:使用 NearestNeighbor 將影像預先縮放至 200%、300% 等等。

使用套用在繫結上的轉換子或 XAML 標記延伸來預先縮放影像。 例如:

<vsui:DpiPrescaleImageSourceConverter x:Key="DpiPrescaleImageSourceConverter" />

<Image Source="{Binding Path=SelectedImage, Converter={StaticResource DpiPrescaleImageSourceConverter}}" Width="16" Height="16" />

<Image Source="{vsui:DpiPrescaledImage Images/Help.png}" Width="16" Height="16" />

如果影像也需要主題 (如果不是全部都需要,大多數應該都需要),標記可以使用不同的轉換子,先設定影像的主題,然後進行預先縮放。 標記可以使用 DpiPrescaleThemedImageConverterDpiPrescaleThemedImageSourceConverter,視所需的轉換輸出而定。

<vsui:DpiPrescaleThemedImageSourceConverter x:Key="DpiPrescaleThemedImageSourceConverter" />

<Image Width="16" Height="16">
  <Image.Source>
    <MultiBinding Converter="{StaticResource DpiPrescaleThemedImageSourceConverter}">
      <Binding Path="Icon" />
      <Binding Path="(vsui:ImageThemingUtilities.ImageBackgroundColor)"
               RelativeSource="{RelativeSource Self}" />
      <Binding Source="{x:Static vsui:Boxes.BooleanTrue}" />
    </MultiBinding>
  </Image.Source>
</Image>

步驟 2:確定目前 DPI 的最終大小是正確的。

因為 WPF 會使用 UIElement 上設定的 BitmapScalingMode 屬性來調整目前 DPI 的 UI,因此使用已預先縮放影像作為其來源的 Image 控制項看起來會比應該的大小大兩到三倍。 以下是幾個抵銷此效果的方法:

  • 如果您知道原始影像的維度為 100%,您可以指定 Image 控制項的確切大小。 這些大小會先反映 UI 的大小,再套用縮放比例。

    <Image Source="{Binding Path=SelectedImage, Converter={StaticResource DpiPrescaleImageSourceConverter}}" Width="16" Height="16" />
    
  • 如果不知道原始影像的大小,則可使用 LayoutTransform 來縮小最終的 Image 物件。 例如:

    <Image Source="{Binding Path=SelectedImage, Converter={StaticResource DpiPrescaleImageSourceConverter}}" >
        <Image.LayoutTransform>
            <ScaleTransform
                ScaleX="{x:Static vsui:DpiHelper.PreScaledImageLayoutTransformScale}"
                ScaleY="{x:Static vsui:DpiHelper.PreScaledImageLayoutTransformScale}" />
        </Image.LayoutTransform>
    </Image>
    

啟用 WebOC 的 HDPI 支援

根據預設,WebOC 控制項 (例如 WPF 中的 WebBrowser 控制項或 IWebBrowser2 介面) 不會啟用 HDPI 偵測和支援。 結果會產生一個顯示內容在高解析度顯示器上太小的內嵌控制項。 下列說明如何在特定的 Web WebOC 執行個體中啟用高 DPI 支援。

實作 IDocHostUIHandler 介面 (請參閱 IDocHostUIHandler 的相關 MSDN 文章:

[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("BD3F23C0-D43E-11CF-893B-00AA00BDCE1A")]
public interface IDocHostUIHandler
{
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int ShowContextMenu(
        [In, MarshalAs(UnmanagedType.U4)] int dwID,
        [In] POINT pt,
        [In, MarshalAs(UnmanagedType.Interface)] object pcmdtReserved,
        [In, MarshalAs(UnmanagedType.IDispatch)] object pdispReserved);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int GetHostInfo([In, Out] DOCHOSTUIINFO info);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int ShowUI(
        [In, MarshalAs(UnmanagedType.I4)] int dwID,
        [In, MarshalAs(UnmanagedType.Interface)] object activeObject,
        [In, MarshalAs(UnmanagedType.Interface)] object commandTarget,
        [In, MarshalAs(UnmanagedType.Interface)] object frame,
        [In, MarshalAs(UnmanagedType.Interface)] object doc);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int HideUI();
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int UpdateUI();
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int EnableModeless([In, MarshalAs(UnmanagedType.Bool)] bool fEnable);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int OnDocWindowActivate([In, MarshalAs(UnmanagedType.Bool)] bool fActivate);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int OnFrameWindowActivate([In, MarshalAs(UnmanagedType.Bool)] bool fActivate);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int ResizeBorder(
        [In] COMRECT rect,
        [In, MarshalAs(UnmanagedType.Interface)] object doc,
        bool fFrameWindow);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int TranslateAccelerator(
        [In] ref MSG msg,
        [In] ref Guid group,
        [In, MarshalAs(UnmanagedType.I4)] int nCmdID);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int GetOptionKeyPath(
        [Out, MarshalAs(UnmanagedType.LPArray)] string[] pbstrKey,
        [In, MarshalAs(UnmanagedType.U4)] int dw);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int GetDropTarget(
        [In, MarshalAs(UnmanagedType.Interface)] IOleDropTarget pDropTarget,
        [MarshalAs(UnmanagedType.Interface)] out IOleDropTarget ppDropTarget);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int GetExternal([MarshalAs(UnmanagedType.IDispatch)] out object ppDispatch);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int TranslateUrl(
        [In, MarshalAs(UnmanagedType.U4)] int dwTranslate,
        [In, MarshalAs(UnmanagedType.LPWStr)] string strURLIn,
        [MarshalAs(UnmanagedType.LPWStr)] out string pstrURLOut);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int FilterDataObject(
        IDataObject pDO,
        out IDataObject ppDORet);
    }

或者,選擇實作 ICustomDoc 介面 (請參閱 ICustomDoc 的相關 MSDN 文章:

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("3050F3F0-98B5-11CF-BB82-00AA00BDCE0B")]
public interface ICustomDoc
{
    void SetUIHandler(IDocHostUIHandler pUIHandler);
}

將實作 IDocHostUIHandler 的類別與 WebOC 的文件建立關聯。 如果您已實作上述的 ICustomDoc 介面,則只要 WebOC 的文件屬性有效,即可將其轉換成 ICustomDoc 並呼叫 SetUIHandler 方法,傳遞實作 IDocHostUIHandler 的類別。

// "this" references that class that owns the WebOC control and in this case also implements the IDocHostUIHandler interface
ICustomDoc customDoc = (ICustomDoc)webBrowser.Document;
customDoc.SetUIHandler(this);

如果您未實作 ICustomDoc 介面,則只要 WebOC 的文件屬性有效,就必須將它轉換成 IOleObject,然後呼叫 SetClientSite 方法,傳入實作 IDocHostUIHandler 的類別。 在傳遞至 GetHostInfo 方法呼叫的 DOCHOSTUIINFO 上設定 DOCHOSTUIFLAG_DPI_AWARE 旗標:

public int GetHostInfo(DOCHOSTUIINFO info)
{
    // This is what the default site provides.
    info.dwFlags = (DOCHOSTUIFLAG)0x5a74012;
    // Add the DPI flag to the defaults
    info.dwFlags |=.DOCHOSTUIFLAG.DOCHOSTUIFLAG_DPI_AWARE;
    return S_OK;
}

取得 WebOC 控制項以支援 HPDI 所需要做的應該就這些。

提示

  1. 如果 WebOC 控制項上的 document 屬性發生變更,您可能需要將文件與 IDocHostUIHandler 類別重新關聯。

  2. 如果上述操作無效,WebOC 不取得 DPI 旗標變更是已知問題。 修正此問題的最可靠方式是切換 WebOC 的光學縮放,也就是以兩個不同的縮放百分比進行兩次呼叫。 此外,如果需要此因應措施,可能需要在每個瀏覽呼叫上執行它。

    // browser2 is a SHDocVw.IWebBrowser2 in this case
    // EX: Call the Exec twice with DPI%-1 and then DPI% as the zoomPercent values
    IOleCommandTarget cmdTarget = browser2.Document as IOleCommandTarget;
    if (cmdTarget != null)
    {
        object commandInput = zoomPercent;
        cmdTarget.Exec(IntPtr.Zero,
                       OLECMDID_OPTICAL_ZOOM,
                       OLECMDEXECOPT_DONTPROMPTUSER,
                       ref commandInput,
                       ref commandOutput);
    }