Suporte de reconhecimento por monitor para extensores do Visual Studio

As versões anteriores ao Visual Studio 2019 tinham seu contexto de reconhecimento de DPI definido como reconhecimento de sistema, em vez de reconhecimento de DPI (PMA) por monitor. A execução em reconhecimento de sistema resultou em uma experiência visual degradada (por exemplo, fontes ou ícones borrados) sempre que o Visual Studio teve que renderizar em monitores com diferentes fatores de escala ou remotamente em máquinas com diferentes configurações de exibição (por exemplo, diferentes dimensionamentos do Windows).

O contexto de reconhecimento de DPI do Visual Studio 2019 é definido como PMA, quando o ambiente oferece suporte a ele, permitindo que o Visual Studio renderize de acordo com a configuração da exibição onde está hospedado, em vez de uma única configuração definida pelo sistema. Em última análise, traduzindo-se em uma interface do usuário sempre nítida para áreas de superfície que oferecem suporte ao modo PMA.

Consulte a documentação High DPI Desktop Application Development on Windows para obter mais informações sobre os termos e o cenário geral abordados neste documento.

Início rápido

  • Verifique se o Visual Studio está sendo executado no modo PMA (consulte Habilitando PMA)

  • Validar se a extensão funciona corretamente em um conjunto de cenários comuns (consulte Testando suas extensões para problemas de PMA)

  • Se você encontrar problemas, poderá usar as estratégias/recomendações discutidas neste documento para diagnosticar e corrigir esses problemas. Você também precisará adicionar o novo pacote NuGet Microsoft.VisualStudio.DpiAwareness ao seu projeto para acessar as APIs necessárias.

Habilitar PMA

Para habilitar o PMA no Visual Studio, os seguintes requisitos precisam ser atendidos:

Depois que esses requisitos forem atendidos, o Visual Studio habilitará automaticamente o modo PMA em todo o processo.

Observação

O conteúdo do Windows Forms no Visual Studio (por exemplo, Navegador de propriedades) oferece suporte a PMA somente quando você tem o Visual Studio 2019 versão 16.1 ou posterior.

Testar suas extensões para problemas de PMA

O Visual Studio oferece suporte oficial às estruturas WPF, Windows Forms, Win32 e HTML/JS UI. Quando o Visual Studio é colocado no modo PMA, cada pilha de interface do usuário se comporta de forma diferente. Portanto, independentemente da estrutura da interface do usuário, é recomendável que uma aprovação no teste seja executada para garantir que toda a interface do usuário seja compatível com o modo PMA.

É recomendável validar os seguintes cenários comuns:

  • Alterar o fator de escala de um único ambiente de monitor enquanto o aplicativo está em execução.

    Esse cenário ajuda a testar se a interface do usuário está respondendo à alteração dinâmica de DPI do Windows.

  • Encaixar/desencaixar um laptop onde um monitor conectado é definido como o principal e o monitor conectado tem um fator de escala diferente do laptop enquanto o aplicativo está em execução.

    Esse cenário ajuda a testar se a interface do usuário está respondendo à alteração de DPI de exibição, bem como a manipular exibições que estão sendo adicionadas ou removidas dinamicamente.

  • Ter vários monitores com diferentes fatores de escala e mover a aplicação entre eles.

    Esse cenário ajuda a testar se a interface do usuário está respondendo à alteração de DPI de exibição

  • Comunicação remota em uma máquina quando as máquinas locais e remotas têm fatores de escala diferentes para o monitor primário.

    Esse cenário ajuda a testar se a interface do usuário está respondendo à alteração dinâmica de DPI do Windows.

Um bom teste preliminar para saber se sua interface do usuário pode ter problemas é se o código utiliza as classes Microsoft.VisualStudio.Utilities.Dpi.DpiHelper, Microsoft.VisualStudio.PlatformUI.DpiHelper ou VsUI::CDpiHelper. Essas classes antigas do DpiHelper suportam apenas o reconhecimento de DPI do sistema e nem sempre funcionam corretamente quando o processo é PMA.

O uso típico desses DpiHelpers será parecido com:

Point screenTopRight = logicalBounds.TopRight.LogicalToDeviceUnits();

POINT screenIntTopRight = new POINT
{
    x = (int)screenTopRIght.X,
    y = (int)screenTopRIght.Y
}

// Declared via P/Invoke
IntPtr monitor = MonitorFromPoint(screenIntTopRight, MONITOR_DEFAULTTONEARST);

No exemplo anterior, um retângulo que representa os limites lógicos de uma janela é convertido em unidades de dispositivo para que possa ser passado para o método nativo MonitorFromPoint que espera coordenadas de dispositivo para retornar um ponteiro de monitor preciso.

Classes de questões

Quando o modo PMA está habilitado para Visual Studio, a interface do usuário pode replicar problemas de várias maneiras comuns. A maioria, se não todos, desses problemas podem acontecer em qualquer uma das estruturas de interface do usuário com suporte do Visual Studio. Além disso, esses problemas também podem acontecer quando uma parte da interface do usuário está sendo hospedada em cenários de dimensionamento de DPI de modo misto (consulte a documentação do Windows para saber mais).

Criação de janela Win32

Ao criar janelas com CreateWindow() ou CreateWindowEx(), um padrão comum é criar a janela em coordenadas (0,0) (o canto superior/esquerdo da exibição principal) e, em seguida, movê-la para sua posição final. No entanto, isso pode fazer com que a janela acione uma mensagem ou evento alterado de DPI, o que pode redisparar outras mensagens ou eventos da interface do usuário e, eventualmente, levar a um comportamento ou renderização indesejados.

Posicionamento do elemento WPF

Ao mover elementos WPF usando o antigo Microsoft.VisualStudio.Utilities.Dpi.DpiHelper, as coordenadas no canto superior esquerdo podem não ser calculadas corretamente sempre que os elementos estiverem em um DPI não primário.

Serialização de tamanhos ou posições de elementos da interface do usuário

Quando o tamanho ou a posição da interface do usuário (se salvo como unidades de dispositivo) for restaurado em um contexto de DPI diferente do que foi armazenado, ele será posicionado e dimensionado incorretamente. Isso acontece porque as unidades de dispositivo têm uma relação de DPI inerente.

Dimensionamento incorreto

Os elementos da interface do usuário criados no DPI primário serão dimensionados corretamente, no entanto, quando movidos para uma exibição com um DPI diferente, eles não são redimensionados e seu conteúdo é muito grande ou muito pequeno.

Limite incorreto

Da mesma forma que o problema de dimensionamento, os elementos da interface do usuário calculam seus limites corretamente em seu contexto de DPI primário, no entanto, quando movidos para um DPI não primário, eles não calculam os novos limites corretamente. Como tal, a janela de conteúdo é muito pequena ou muito grande em comparação com a interface do usuário de hospedagem, o que resulta em espaço vazio ou recorte.

Arrastar & soltar

Sempre que dentro de cenários de DPI de modo misto (por exemplo, renderização de diferentes elementos da interface do usuário em diferentes modos de reconhecimento de DPI), as coordenadas de arrastar e soltar podem ser calculadas incorretamente, resultando na posição final de soltar que acaba incorreta.

Interface do usuário fora do processo

Algumas interfaces do usuário são criadas fora do processo e, se o processo externo de criação estiver em um modo de reconhecimento de DPI diferente do Visual Studio, isso poderá introduzir qualquer um dos problemas de renderização anteriores.

Controles, imagens ou layouts do Windows Forms renderizados incorretamente

Nem todo o conteúdo do Windows Forms oferece suporte ao modo PMA. Como resultado, você pode ver um problema de renderização com layouts ou dimensionamento incorretos. Uma solução possível nesse caso é renderizar explicitamente o conteúdo do Windows Forms em "System Aware" DpiAwarenessContext (consulte Forçar um controle em um DpiAwarenessContext específico).

Controles do Windows Forms ou janelas não exibidas

Uma das principais causas para esse problema é os desenvolvedores tentando recriar um controle ou janela com um DpiAwarenessContext para uma janela com um DpiAwarenessContext diferente.

As imagens a seguir mostram as restrições padrão atuais do sistema operacional Windows nas janelas pai:

A screenshot of the correct parenting behavior

Observação

Você pode alterar esse comportamento definindo o comportamento de hospedagem de thread (consulte Dpi_Hosting_Behavior enumeração).

Como resultado, se você definir a relação pai-filho entre modos sem suporte, ela falhará e o controle ou a janela pode não ser renderizada conforme o esperado.

Diagnosticar problemas

Há muitos fatores a serem considerados ao identificar problemas relacionados à PMA:

  • A interface do usuário ou a API espera valores lógicos ou de dispositivo?

    • A interface do usuário e as APIs do WPF normalmente usam valores lógicos (mas nem sempre)
    • A interface do usuário e as APIs do Win32 normalmente usam valores de dispositivo
  • De onde vêm os valores?

    • Se estiver recebendo valores de outra interface do usuário ou API, ele está passando valores lógicos ou de dispositivo.
    • Se receberem valores de várias fontes, todos eles usam/esperam os mesmos tipos de valores ou as conversões precisam ser misturadas e combinadas?
  • As constantes da interface do usuário estão em uso e de que forma estão?

  • O thread está no contexto de DPI correto para os valores que está recebendo?

    As alterações para habilitar a hospedagem de DPI mista geralmente devem colocar os caminhos de código no contexto correto, no entanto, o trabalho feito fora do loop de mensagem principal ou do fluxo de eventos pode ser executado no contexto de DPI errado.

  • Os valores ultrapassam os limites do contexto DPI?

    Arrastar & soltar é uma situação comum em que as coordenadas podem cruzar contextos DPI. A janela tenta fazer a coisa certa, mas, em alguns casos, a interface do usuário do host pode precisar fazer o trabalho de conversão para garantir a correspondência de limites de contexto.

Pacote PMA NuGet

As novas bibliotecas DpiAwarness podem ser encontradas no pacote NuGet Microsoft.VisualStudio.DpiAwareness.

As ferramentas a seguir podem ajudar a depurar problemas relacionados ao PMA em algumas das diferentes pilhas de interface do usuário suportadas pelo Visual Studio.

Snoop

O Snoop é uma ferramenta de depuração XAML que tem algumas funcionalidades extras que as ferramentas XAML internas do Visual Studio não têm. Além disso, o Snoop não precisa depurar ativamente o Visual Studio para poder exibir e ajustar sua interface do usuário do WPF. As duas principais maneiras pelas quais o Snoop pode ser útil para diagnosticar problemas de PMA é para validar coordenadas de posicionamento lógico ou limites de tamanho e para validar se a interface do usuário tem o DPI correto.

Ferramentas XAML do Visual Studio

Como o Snoop, as ferramentas XAML no Visual Studio podem ajudar a diagnosticar problemas de PMA. Depois que um provável culpado for encontrado, você poderá definir pontos de interrupção e usar a janela Live Visual Tree, bem como as janelas de depuração, para inspecionar os limites da interface do usuário e o DPI atual.

Estratégias para corrigir problemas de PMA

Substituir chamadas DpiHelper

Na maioria dos casos, corrigir problemas de interface do usuário no modo PMA se resume a substituir chamadas em código gerenciado para as antigas classes Microsoft.VisualStudio.Utilities.Dpi.DpiHelper e Microsoft.VisualStudio.PlatformUI.DpiHelper, com chamadas para a nova classe auxiliar Microsoft.VisualStudio.Utilities.DpiAwareness.

// Remove this kind of use:
Point deviceTopLeft = new Point(window.Left, window.Top).LogicalToDeviceUnits();

// Replace with this use:
Point deviceTopLeft = window.LogicalToDevicePoint(new Point(window.Left, window.Top));

Para código nativo, isso implicará na substituição de chamadas para a antiga classe VsUI::CDpiHelper por chamadas para a nova classe VsUI::CDpiAwareness.

// Remove this kind of use:
int cx = VsUI::DpiHelper::LogicalToDeviceUnitsX(m_cxS);
int cy = VsUI::DpiHelper::LogicalToDeviceUnitsY(m_cyS);

// Replace with this use:
int cx = m_cxS;
int cy = m_cyS;
VsUI::CDpiAwareness::LogicalToDeviceUnitsX(m_hwnd, &cx);
VsUI::CDpiAwareness::LogicalToDeviceUnitsY(m_hwnd, &cy);

As novas classes DpiAwareness e CDpiAwareness oferecem os mesmos auxiliares de conversão de unidade que as classes DpiHelper, mas exigem um parâmetro de entrada adicional: o elemento UI a ser usado como referência para a operação de conversão. É importante observar que os auxiliares de dimensionamento de imagem não existem nos novos auxiliares DpiAwareness/CDpiAwareness e, se necessário, o ImageService deve ser usado.

A classe DpiAwareness gerenciada oferece auxiliares para WPF Visuals, Windows Forms Controls e Win32 HWNDs e HMONITORs (ambos na forma de IntPtrs), enquanto a classe CDpiAwareness nativa oferece auxiliares HWND e HMONITOR.

Caixas de diálogo, janelas ou controles do Windows Forms exibidos no DpiAwarenessContext errado

Mesmo depois de um pai bem-sucedido de janelas com diferentes DpiAwarenessContexts (devido ao comportamento padrão do Windows), os usuários ainda podem ver problemas de dimensionamento à medida que janelas com diferentes DpiAwarenessContexts são dimensionadas de forma diferente. Como resultado, os usuários podem ver problemas de alinhamento/texto desfocado ou imagem na interface do usuário.

A solução é definir o escopo DpiAwarenessContext correto para todas as janelas e controles no aplicativo.

Caixas de diálogo de modo misto de nível superior (TLMM)

Ao criar janelas de nível superior, como caixas de diálogo modais, é importante verificar se o thread está no estado correto antes da janela (e seu identificador) ser criada. O thread pode ser colocado em reconhecimento de sistema usando o auxiliar CDpiScope em nativo ou o auxiliar DpiAwareness.EnterDpiScope em gerenciado. (O TLMM geralmente deve ser usado em caixas de diálogo/janelas não-WPF.)

Modo misto de nível infantil (CLMM)

Por padrão, as janelas filhas recebem o contexto de reconhecimento de DPI de thread atual, se criado sem um pai, ou o contexto de reconhecimento de DPI do pai, quando criado com um pai. Para criar uma criança com um contexto de reconhecimento de DPI diferente de seu pai, o thread pode ser colocado no contexto de reconhecimento de DPI desejado. Em seguida, a criança pode ser criada sem um pai e manualmente reparentada para a janela pai.

Problemas de CLMM

A maior parte do trabalho de cálculo da interface do usuário que acontece como parte do loop de mensagens principal ou da cadeia de eventos já deve estar em execução no contexto de reconhecimento de DPI correto. No entanto, se os cálculos de coordenadas ou dimensionamento forem feitos fora desses fluxos de trabalho principais (como durante uma tarefa de tempo ocioso ou fora do thread da interface do usuário, o contexto de reconhecimento de DPI atual pode estar incorreto, levando a problemas de deslocamento ou dimensionamento incorreto da interface do usuário. Colocar o thread no estado correto para o trabalho da interface do usuário geralmente corrige o problema.

Desativar o CLMM

Se uma janela de ferramenta não-WPF estiver sendo migrada para oferecer suporte total ao PMA, ela precisará desativar o CLMM. Para isso, uma nova interface precisa ser implementada: IVsDpiAware.

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IVsDpiAware
{
    [ComAliasName("Microsoft.VisualStudio.Shell.Interop.VSDPIMode")]
    uint Mode {get;}
}
IVsDpiAware : public IUnknown
{
    public:
        HRRESULT STDMETHODCALLTYPE get_Mode(__RCP__out VSDPIMODE *dwMode);
};

Para linguagens gerenciadas, o melhor lugar para implementar essa interface é na mesma classe que deriva de Microsoft.VisualStudio.Shell.ToolWindowPane. Para C++, o melhor lugar para implementar essa interface é na mesma classe que implementa IVsWindowPane de vsshell.h.

O valor retornado pela propriedade Mode na interface é um __VSDPIMODE (e convertido para um uint em gerenciado):

enum __VSDPIMODE
{
    VSDM_Unaware    = 0x01,
    VSDM_System     = 0x02,
    VSDM_PerMonitor = 0x03,
}
  • Inconsciente significa que a janela de ferramenta precisa lidar com 96 DPI, o Windows lidará com o dimensionamento para todos os outros DPIs. Resultando em conteúdo ligeiramente embaçado.
  • Sistema significa que a janela de ferramenta precisa lidar com o DPI para o DPI de exibição principal. Qualquer tela com um DPI correspondente parecerá nítida, mas se o DPI for diferente ou for alterado durante a sessão, o Windows manipulará o dimensionamento e ficará ligeiramente desfocado.
  • PerMonitor significa que a janela de ferramentas precisa lidar com todos os DPIs em todos os monitores e sempre que o DPI for alterado.

Observação

O Visual Studio oferece suporte apenas ao reconhecimento de PerMonitorV2, portanto, o valor de enum PerMonitor é convertido para o valor de Windows de DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2.

Forçar um controle em um DpiAwarenessContext específico

A interface do usuário herdada que não está sendo atualizada para oferecer suporte ao modo PMA, ainda pode precisar de pequenos ajustes para funcionar enquanto o Visual Studio estiver sendo executado no modo PMA. Uma dessas correções envolve verificar se a interface do usuário está sendo criada no DpiAwarenessContext correto. Para forçar sua interface do usuário em um DpiAwarenessContext específico, você pode inserir um escopo de DPI com o seguinte código:

using (DpiAwareness.EnterDpiScope(DpiAwarenessContext.SystemAware))
{
    Form form = new MyForm();
    form.ShowDialog();
}
void MyClass::ShowDialog()
{
    VsUI::CDpiScope dpiScope(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE);
    HWND hwnd = ::CreateWindow(...);
}

Observação

Forçar o DpiAwarenessContext só funciona em caixas de diálogo WPF e WPF de nível superior não-WPF. Ao criar a interface do usuário do WPF que deve ser hospedada dentro de janelas de ferramentas ou designers, assim que o conteúdo é inserido na árvore da interface do usuário do WPF, ele é convertido para o processo atual DpiAwarenessContext.

Problemas conhecidos

Windows Forms

Para otimizar os novos cenários de modo misto, o Windows Forms alterou a forma como cria controles e janelas sempre que seu pai não foi definido explicitamente. Anteriormente, os controles sem um pai explícito usavam uma "Janela de estacionamento" interna como um pai temporário para o controle ou janela que estava sendo criado.

Antes do .NET 4.8, havia uma única "Janela de estacionamento" que obtém seu DpiAwarenessContext do contexto de reconhecimento de DPI de thread atual no momento da criação da janela. Qualquer controle não parentado herda o mesmo DpiAwarenessContext que a janela de estacionamento quando o identificador do controle é criado e seria reparentado para o pai final/esperado pelo desenvolvedor do aplicativo. Isso causaria falhas baseadas em tempo se a "Janela de estacionamento" tivesse um DpiAwarenessContext mais alto do que a janela pai final.

A partir do .NET 4.8, agora há uma "janela de estacionamento" para cada DpiAwarenessContext que foi encontrado. A outra grande diferença é que o DpiAwarenessContext usado para o controle é armazenado em cache quando o controle é criado, não quando o identificador é criado. Isso significa que o comportamento final geral é o mesmo, mas pode transformar o que costumava ser um problema baseado em tempo em um problema consistente. Ele também dá ao desenvolvedor de aplicativos um comportamento mais determinístico para escrever seu código de interface do usuário e defini-lo corretamente.