Partilhar via


Resolver problemas de DPI

Um número crescente de dispositivos está sendo enviado com telas de "alta resolução". Essas telas normalmente têm mais de 200 pixels por polegada (ppi). Trabalhar com um aplicativo nesses computadores exigirá que o conteúdo seja ampliado para atender às necessidades de visualização do conteúdo a uma distância de visualização normal para o dispositivo. A partir de 2014, o principal alvo para monitores de alta densidade são dispositivos de computação móvel (tablets, laptops e telefones).

O Windows 8.1 e superior contém vários recursos para permitir que essas máquinas trabalhem com monitores e ambientes em que a máquina está conectada a monitores de alta densidade e densidade padrão ao mesmo tempo.

  • O Windows pode permitir que você dimensione o conteúdo para o dispositivo usando a configuração "Aumentar ou diminuir o texto e outros itens" (disponível desde o Windows XP).

  • O Windows 8.1 e versões superiores dimensionarão automaticamente o conteúdo para que a maioria dos aplicativos seja consistente quando movido entre telas de diferentes densidades de pixel. Quando a tela primária é de alta densidade (200% de dimensionamento) e a tela secundária é de densidade padrão (100%), o Windows dimensionará automaticamente o conteúdo da janela do aplicativo para baixo na exibição secundária (1 pixel exibido para cada 4 pixels renderizados pelo aplicativo).

  • O Windows terá como padrão o dimensionamento correto para a densidade de pixels e a distância de visualização da tela (Windows 7 e superior, configurável por OEM).

  • O Windows pode escalar automaticamente o conteúdo até 250% em novos dispositivos que excedam 280 ppi (a partir do Windows 8.1 S14).

    O Windows tem uma maneira de lidar com a ampliação da interface do usuário para aproveitar o aumento da contagem de pixels. Um aplicativo opta por esse sistema declarando-se "ciente de DPI do sistema". Os aplicativos que não fazem isso são ampliados pelo sistema. Isso pode resultar em uma experiência de usuário "confusa", onde todo o aplicativo é uniformemente esticado em pixels. Por exemplo:

    DPI Issues Fuzzy

    O Visual Studio aceita ter reconhecimento de dimensionamento de DPI e, portanto, não é "virtualizado".

    O Windows (e o Visual Studio) aproveitam várias tecnologias de interface do usuário, que têm maneiras diferentes de lidar com fatores de dimensionamento definidos pelo sistema. Por exemplo:

  • O WPF mede os controles de forma independente do dispositivo (unidades, não pixels). A interface do usuário do WPF é dimensionada automaticamente para o DPI atual.

  • Todos os tamanhos de texto, independentemente da estrutura da interface do usuário, são expressos em pontos e, portanto, são tratados pelo sistema como independentes de DPI. O texto no Win32, WinForms e WPF já é dimensionado corretamente quando desenhado para o dispositivo de exibição.

  • As caixas de diálogo e janelas do Win32/WinForms têm meios para habilitar o layout que é redimensionado com texto (por exemplo, por meio de painéis de layout de grade, fluxo e tabela). Isso permite evitar locais de pixels codificados que não são dimensionados quando os tamanhos de fonte são aumentados.

  • Ícones fornecidos pelo sistema ou recursos baseados em métricas do sistema (por exemplo, SM_CXICON e SM_CXSMICON) já estão ampliados.

Interface do usuário baseada em Win32 (GDI, GDI+) e WinForms mais antiga

Embora o WPF já tenha alto reconhecimento de DPI, grande parte do nosso código baseado em Win32/GDI não foi originalmente escrito com o reconhecimento de DPI em mente. O Windows forneceu APIs de dimensionamento de DPI. Correções para problemas Win32 devem usá-los consistentemente em todo o produto. O Visual Studio forneceu uma biblioteca de classes auxiliar para evitar a duplicação de funcionalidade e garantir a consistência em todo o produto.

Imagens de alta resolução

Esta seção é principalmente para desenvolvedores que estendem o Visual Studio 2013. Para o Visual Studio 2015, use o serviço de imagem que é interno do Visual Studio. Você também pode achar que você precisa dar suporte/destino a muitas versões do Visual Studio e, portanto, usar o serviço de imagem em 2015 não é uma opção, pois ele não existe em versões anteriores. Esta seção também é para você então.

Dimensionamento de imagens muito pequenas

Imagens muito pequenas podem ser ampliadas e renderizadas no GDI e WPF usando alguns métodos comuns. As classes auxiliares de DPI gerenciadas estão disponíveis para integradores internos e externos do Visual Studio para abordar ícones de dimensionamento, bitmaps, imagetrips e imagelists. C/C++helpers nativos baseados em Win32 estão disponíveis para dimensionar HICON, HBITMAP, HIMAGELIST e VsUI::GdiplusImage. O dimensionamento de um bitmap normalmente requer apenas uma alteração de uma linha depois de incluir uma referência à biblioteca auxiliar. Por exemplo:

(WinForms) DpiHelper.LogicalToDeviceUnits(ref image);

O dimensionamento de uma imagelist depende se a imagelist está concluída no tempo de carregamento ou se é acrescentada em tempo de execução. Se concluída no tempo de carregamento, chame LogicalToDeviceUnits() com a imagelist como faria com um bitmap. Quando o código precisar carregar um bitmap individual antes de compor a imagelist, certifique-se de dimensionar o tamanho da imagem da imagelist:

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

No código nativo, as dimensões podem ser dimensionadas ao criar a imagelist da seguinte maneira:

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

As funções na biblioteca permitem especificar o algoritmo de redimensionamento. Ao dimensionar imagens a serem colocadas em listas de imagens, certifique-se de especificar a cor de plano de fundo usada para transparência ou use o dimensionamento NearestNeighbor (que causará distorções em 125% e 150%).

Consulte a DpiHelper documentação no MSDN.

A tabela a seguir mostra exemplos de como as imagens devem ser dimensionadas nos fatores de dimensionamento de DPI correspondentes. As imagens descritas em laranja denotam nossa prática recomendada a partir do Visual Studio 2013 (dimensionamento de 100%-200% de DPI):

DPI Issues Scaling

Problemas de layout

Problemas comuns de layout podem ser evitados principalmente mantendo os pontos na interface do usuário dimensionados e relativos uns aos outros, em vez de usar locais absolutos (especificamente, em unidades de pixel). Por exemplo:

  • As posições de layout/texto precisam ser ajustadas para levar em conta as imagens ampliadas.

  • As colunas em grades precisam ter larguras ajustadas para o texto ampliado.

  • Tamanhos codificados ou espaço entre elementos também precisarão ser ampliados. Os tamanhos baseados apenas nas dimensões do texto normalmente são bons, porque as fontes são dimensionadas automaticamente.

    As funções auxiliares estão disponíveis na DpiHelper classe para permitir o dimensionamento nos eixos X e Y:

  • LogicalToDeviceUnitsX/LogicalToDeviceUnitsY (funções permitem dimensionamento no eixo X/Y)

  • espaço int = DpiHelper.LogicalToDeviceUnitsX (10);

  • altura int = VsUI::D piHelper::LogicalToDeviceUnitsY(5);

    Há sobrecargas LogicalToDeviceUnits para permitir o dimensionamento de objetos como Rect, Point e Size.

Usando a biblioteca/classe DPIHelper para dimensionar imagens e layout

A biblioteca auxiliar de DPI do Visual Studio está disponível em formulários nativos e gerenciados e pode ser usada fora do shell do Visual Studio por outros aplicativos.

Para usar a biblioteca, vá para os exemplos de extensibilidade VSSDK do Visual Studio e clone o exemplo de DPI_Images_Icons alto.

Nos arquivos de origem, inclua VsUIDpiHelper.h e chame as funções estáticas da VsUI::DpiHelper classe:

#include "VsUIDpiHelper.h"

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

Observação

Não use as funções auxiliares em variáveis estáticas de nível de módulo ou de classe. A biblioteca também usa estática para sincronização de threads e você pode ter problemas de inicialização de pedidos. Converta essas estáticas em variáveis de membro não estáticas ou envolva-as em uma função (para que elas sejam construídas no primeiro acesso).

Para acessar as funções auxiliares de DPI do código gerenciado que será executado dentro do ambiente do Visual Studio:

  • O projeto de consumo deve fazer referência à versão mais recente do Shell MPF. Por exemplo:

    <Reference Include="Microsoft.VisualStudio.Shell.14.0.dll" />
    
  • Verifique se o projeto tem referências a System.Windows.Forms, PresentationCore e PresentationUI.

  • No código, use o namespace Microsoft.VisualStudio.PlatformUI e chame funções estáticas da classe DpiHelper. Para tipos com suporte (pontos, tamanhos, retângulos e assim por diante), há funções de extensão fornecidas que retornam novos objetos dimensionados. Por exemplo:

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

Lidando com a confusão de imagem do WPF na interface do usuário com zoom

No WPF, os bitmaps são redimensionados automaticamente pelo WPF para o nível de zoom DPI atual usando um algoritmo bicúbico de alta qualidade (padrão), que funciona bem para imagens ou capturas de tela grandes, mas é inadequado para ícones de item de menu porque introduz confusão percebida.

Recomendações:

  • Para a imagem do logotipo e o trabalho artístico dos banners, o modo de redimensionamento padrão BitmapScalingMode pode ser usado.

  • Para itens de menu e imagens de iconografia, o BitmapScalingMode deve ser usado quando não causar outros artefatos de distorção para eliminar a confusão (em 200% e 300%).

  • Para grandes níveis de zoom e não múltiplos de 100% (por exemplo, 250% ou 350%), dimensionar imagens iconográficas com resultados bicúbicos resulta em uma interface do usuário confusa e desbotada. Um resultado melhor é obtido primeiro dimensionando a imagem com NearestNeighbor para o maior múltiplo de 100% (por exemplo, 200% ou 300%) e dimensionando com bicúbico a partir daí. Consulte Caso especial: pré-dimensionamento de imagens WPF para grandes níveis de DPI para obter mais informações.

    A classe DpiHelper no namespace Microsoft.VisualStudio.PlatformUI fornece um membro BitmapScalingMode que pode ser usado para associação. Ele permitirá que o shell do Visual Studio controle o modo de dimensionamento de bitmap no produto uniformemente, dependendo do fator de dimensionamento de DPI.

    Para usá-lo em XAML, adicione:

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

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

O shell do Visual Studio já define essa propriedade em janelas e caixas de diálogo de nível superior. A interface do usuário baseada em WPF em execução no Visual Studio já a herdará. Se a configuração não se propagar para suas partes específicas da interface do usuário, ela poderá ser definida no elemento raiz da interface do usuário XAML/WPF. Os locais onde isso acontece incluem pop-ups, em elementos com pais Win32 e janelas de designer que ficam sem processo, como o Blend.

Algumas interfaces do usuário podem ser dimensionadas independentemente do nível de zoom DPI definido pelo sistema, como o editor de texto do Visual Studio e designers baseados em WPF (WPF Desktop e Windows Store). Nesses casos, DpiHelper.BitmapScalingMode não deve ser usado. Para corrigir esse problema no editor, a equipe do IDE criou uma propriedade personalizada intitulada RenderOptions.BitmapScalingMode. Defina esse valor de propriedade como HighQuality ou NearestNeighbor, dependendo do nível de zoom combinado do sistema e da interface do usuário.

Caso especial: presdimensionamento de imagens WPF para grandes níveis de DPI

Para níveis de zoom muito grandes que não são múltiplos de 100% (por exemplo, 250%, 350% e assim por diante), dimensionar imagens de iconografia com resultados bicúbicos em uma interface do usuário confusa e desbotada. A impressão dessas imagens ao lado de um texto nítido é quase como a de uma ilusão de ótica. As imagens parecem estar mais próximas do olho e fora de foco em relação ao texto. O resultado do dimensionamento nesse tamanho ampliado pode ser melhorado primeiro dimensionando a imagem com NearestNeighbor para o maior múltiplo de 100% (por exemplo, 200% ou 300%) e dimensionando com bicúbico para o restante (mais 50%).

A seguir está um exemplo das diferenças nos resultados, onde a primeira imagem é dimensionada com o algoritmo de escala dupla aprimorado 100%-200%-250%, e a segunda apenas com 100%->>>250% bicúbicos.

DPI Issues Double Scaling Example

Para permitir que a interface do usuário use esse dimensionamento duplo, a marcação XAML para exibir cada elemento Image precisará ser modificada. Os exemplos a seguir demonstram como usar o dimensionamento duplo no WPF no Visual Studio usando a biblioteca DpiHelper e Shell.12/14.

Etapa 1: pré-dimensione a imagem para 200%, 300% e assim por diante usando NearestNeighbor.

Pré-dimensione a imagem usando um conversor aplicado em uma associação ou com uma extensão de marcação XAML. Por exemplo:

<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" />

Se a imagem também precisa ser temática (a maioria, se não todas, deveriam), a marcação pode usar um conversor diferente que primeiro faz o tema da imagem e, em seguida, o pré-dimensionamento. A marcação pode usar um DpiPrescaleThemedImageConverter ou DpiPrescaleThemedImageSourceConverter, dependendo da saída de conversão desejada.

<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>

Etapa 2: Verifique se o tamanho final está correto para o DPI atual.

Como o WPF dimensionará a interface do usuário para o DPI atual usando a propriedade BitmapScalingMode definida no UIElement, um controle Image usando uma imagem prescal como origem parecerá duas ou três vezes maior do que deveria. A seguir estão algumas maneiras de combater esse efeito:

  • Se você souber a dimensão da imagem original em 100%, poderá especificar o tamanho exato do controle Image. Esses tamanhos refletirão o tamanho da interface do usuário antes que o dimensionamento seja aplicado.

    <Image Source="{Binding Path=SelectedImage, Converter={StaticResource DpiPrescaleImageSourceConverter}}" Width="16" Height="16" />
    
  • Se o tamanho da imagem original não for conhecido, um LayoutTransform poderá ser usado para reduzir a escala do objeto Image final. Por exemplo:

    <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>
    

Habilitando o suporte HDPI para o WebOC

Por padrão, os controles WebOC (como o controle WebBrowser no WPF ou a interface IWebBrowser2) não habilitam a detecção e o suporte a HDPI. O resultado será um controle incorporado com conteúdo de exibição muito pequeno em uma tela de alta resolução. A seguir descreve como habilitar o suporte a DPI alto em uma instância específica do WebOC da Web.

Implemente a interface IDocHostUIHandler (consulte o artigo do MSDN sobre IDocHostUIHandler:

[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);
    }

Opcionalmente, implemente a interface ICustomDoc (consulte o artigo do MSDN sobre o ICustomDoc:

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

Associe a classe que implementa IDocHostUIHandler ao documento do WebOC. Se você implementou a interface ICustomDoc acima, assim que a propriedade document do WebOC for válida, converta-a em um ICustomDoc e chame o método SetUIHandler, passando a classe que implementa 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);

Se você NÃO implementou a interface ICustomDoc, assim que a propriedade document do WebOC for válida, será necessário convertê-la em um IOleObject e chamar o SetClientSite método, passando a classe que implementa IDocHostUIHandler. Defina o sinalizador DOCHOSTUIFLAG_DPI_AWARE no DOCHOSTUIINFO passado para a chamada do GetHostInfo método:

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;
}

Isso deve ser tudo o que você precisa para obter seu controle WebOC para suportar HPDI.

Dicas

  1. Se a propriedade document no controle WebOC for alterada, talvez seja necessário reassociar o documento à classe IDocHostUIHandler.

  2. Se o acima não funcionar, há um problema conhecido com o WebOC não pegando a alteração para o sinalizador de DPI. A maneira mais confiável de corrigir isso é alternar o zoom óptico do WebOC, ou seja, duas chamadas com dois valores diferentes para a porcentagem de zoom. Além disso, se essa solução alternativa for necessária, talvez seja necessário executá-la em cada chamada de navegação.

    // 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);
    }