Desenvolvimento de aplicativos de desktop de alto DPI no Windows
Este conteúdo destina-se a programadores que procuram atualizar aplicações de ambiente de trabalho para lidar com alterações dinâmicas do fator de escala de visualização (pontos por polegada ou DPI), permitindo que as suas aplicações sejam nítidas em qualquer ecrã em que sejam renderizadas.
Para começar, se você estiver criando um novo aplicativo do Windows do zero, é altamente recomendável criar um aplicativo Plataforma Universal do Windows (UWP). Os aplicativos UWP são dimensionados automaticamente — e dinamicamente — para cada monitor em que estão sendo executados.
Aplicações de ambiente de trabalho que utilizam tecnologias de programação mais antigas do Windows (programação Win32 bruta, Windows Forms, Windows Presentation Framework (WPF), etc.) não conseguem lidar automaticamente com o dimensionamento de DPI sem trabalho adicional do desenvolvedor. Sem esse trabalho, os aplicativos aparecerão borrados ou incorretamente dimensionados em muitos cenários de uso comum. Este documento fornece contexto e informações sobre o que está envolvido na atualização de um aplicativo de desktop para renderizar corretamente.
Fator de escala de exibição & DPI
À medida que a tecnologia de exibição progrediu, os fabricantes de painéis de exibição têm empacotado um número crescente de pixels em cada unidade de espaço físico em seus painéis. Isso resultou em pontos por polegada (DPI) dos painéis de exibição modernos sendo muito mais altos do que têm sido historicamente. No passado, a maioria dos monitores tinha 96 pixels por polegada linear de espaço físico (96 DPI); em 2017, monitores com quase 300 DPI ou superior estão prontamente disponíveis.
A maioria das estruturas de interface do usuário de desktop herdadas tem suposições internas de que o DPI de exibição não será alterado durante o tempo de vida do processo. Essa suposição não é mais verdadeira, com DPIs de exibição geralmente mudando várias vezes ao longo da vida útil de um processo de aplicativo. Alguns cenários comuns em que o fator de escala de exibição/DPI muda são:
- Configurações de vários monitores em que cada monitor tem um fator de escala diferente e o aplicativo é movido de um monitor para outro (como um monitor 4K e um monitor 1080p)
- Encaixar e desencaixar um laptop de alto DPI com um monitor externo de baixo DPI (ou vice-versa)
- Ligação através do Ambiente de Trabalho Remoto a partir de um portátil/tablet com DPI elevado para um dispositivo com DPI baixo (ou vice-versa)
- Fazer com que as configurações do fator de escala de exibição sejam alteradas enquanto os aplicativos estão em execução
Nesses cenários, os aplicativos UWP se redesenham para o novo DPI automaticamente. Por predefinição, e sem trabalho adicional do programador, as aplicações de ambiente de trabalho não. Os aplicativos de área de trabalho que não fazem esse trabalho extra para responder às alterações de DPI podem parecer desfocados ou dimensionados incorretamente para o usuário.
Modo de Reconhecimento de DPI
Os aplicativos da área de trabalho devem informar ao Windows se eles suportam o dimensionamento de DPI. Por padrão, o sistema considera os aplicativos da área de trabalho que o DPI não conhece e o bitmap estende suas janelas. Ao definir um dos seguintes modos de reconhecimento de DPI disponíveis, os aplicativos podem dizer explicitamente ao Windows como desejam lidar com o dimensionamento de DPI:
DPI inconsciente
Os aplicativos sem reconhecimento de DPI são renderizados a um valor fixo de DPI de 96 (100%). Sempre que esses aplicativos forem executados em uma tela com uma escala de exibição maior que 96 DPI, o Windows estenderá o bitmap do aplicativo para o tamanho físico esperado. Isso faz com que o aplicativo pareça desfocado.
Reconhecimento de DPI do sistema
Os aplicativos de desktop com reconhecimento de DPI do sistema normalmente recebem o DPI do monitor principal conectado no momento da entrada do usuário. Durante a inicialização, eles estabelecem sua interface do usuário adequadamente (dimensionando controles, escolhendo tamanhos de fonte, carregando ativos, etc.) usando esse valor de DPI do sistema. Como tal, os aplicativos com reconhecimento de DPI do sistema não são dimensionados por DPI (bitmap esticado) pelo Windows na renderização de monitores nesse único DPI. Quando o aplicativo é movido para uma exibição com um fator de escala diferente, ou se o fator de escala de exibição for alterado, o Windows dimensionará as janelas do aplicativo, fazendo com que elas pareçam desfocadas. Efetivamente, os aplicativos de desktop com reconhecimento de DPI do sistema só são renderizados de forma nítida em um único fator de escala de exibição, tornando-se desfocados sempre que o DPI muda.
Consciência de DPI de Per-Monitor e Per-Monitor (V2)
Recomenda-se que os aplicativos de desktop sejam atualizados para usar o modo de reconhecimento de DPI por monitor, permitindo que eles sejam renderizados imediatamente corretamente sempre que o DPI for alterado. Quando um aplicativo informa ao Windows que deseja ser executado nesse modo, o Windows não estenderá o bitmap do aplicativo quando o DPI for alterado, em vez disso, enviará WM_DPICHANGED para a janela do aplicativo. É então da inteira responsabilidade do aplicativo lidar com o redimensionamento para o novo DPI. A maioria das estruturas de interface do usuário usadas por aplicativos de área de trabalho (controles comuns do Windows (comctl32), Windows Forms, Windows Presentation Framework, etc.) não suportam o dimensionamento automático de DPI, exigindo que os desenvolvedores redimensionem e reposicionem o conteúdo de suas próprias janelas.
Existem duas versões de reconhecimento de Per-Monitor que um aplicativo pode se registrar como: versão 1 e versão 2 (PMv2). Registrar um processo como em execução no modo de reconhecimento PMv2 resulta em:
- O aplicativo sendo notificado quando o DPI muda (HWNDs filho e de nível superior)
- O aplicativo que vê os pixels brutos de cada exibição
- O aplicativo nunca sendo bitmap dimensionado pelo Windows
- Área automática não cliente (legenda da janela, barras de rolagem, etc.) Dimensionamento de DPI pelo Windows
- Caixas de diálogo Win32 (do CreateDialog) automaticamente DPI dimensionado pelo Windows
- Ativos de bitmap desenhados por tema em controles comuns (caixas de seleção, planos de fundo de botão, etc.) sendo renderizados automaticamente no fator de escala DPI apropriado
Quando executados no modo de reconhecimento Per-Monitor v2, os aplicativos são notificados quando seu DPI é alterado. Se um aplicativo não se redimensionar para o novo DPI, a interface do usuário do aplicativo aparecerá muito pequena ou muito grande (dependendo da diferença nos valores de DPI anteriores e novos).
Observação
Per-Monitor consciência V1 (PMv1) é muito limitada. Recomenda-se que as aplicações utilizem PMv2.
A tabela a seguir mostra como os aplicativos serão renderizados em diferentes cenários:
Modo de Reconhecimento de DPI | Versão do Windows Introduzida | Visão do aplicativo do DPI | Comportamento na mudança de DPI |
---|---|---|---|
Inconsciente | N/A | Todos os monitores são de 96 DPI | Alongamento de bitmap (desfocado) |
Sistema | Panorama | Todos os monitores têm o mesmo DPI (o DPI do monitor principal no momento em que a sessão do usuário atual foi iniciada) | Alongamento de bitmap (desfocado) |
Per-Monitor | 8.1 | O DPI da tela na qual a janela do aplicativo está localizada principalmente |
|
Per-Monitor V2 | Atualização para criadores do Windows 10 (1703) | O DPI da tela na qual a janela do aplicativo está localizada principalmente |
Escalonamento automático de DPI de:
|
Reconhecimento de DPI por monitor (V1)
Per-Monitor modo de reconhecimento de DPI V1 (PMv1) foi introduzido com o Windows 8.1. Este modo de reconhecimento de DPI é muito limitado e oferece apenas a funcionalidade listada abaixo. Recomenda-se que as aplicações de ambiente de trabalho utilizem Per-Monitor modo de reconhecimento v2, suportado no Windows 10 1703 ou superior.
O suporte inicial para reconhecimento por monitor oferecia apenas os seguintes aplicativos:
- Os HWNDs de nível superior são notificados de uma alteração de DPI e recebem um novo tamanho sugerido
- O Windows não estenderá o bitmap da interface do usuário do aplicativo
- O aplicativo vê todos os monitores em pixels físicos (consulte virtualização)
No Windows 10 1607 ou superior, os aplicativos PMv1 também podem chamar EnableNonClientDpiScaling durante a WM_NCCREATE para solicitar que o Windows dimensione corretamente a área não cliente da janela.
Por Monitor DPI Scaling Suporte por UI Framework / Tecnologia
A tabela abaixo mostra o nível de suporte de reconhecimento de DPI por monitor oferecido por várias estruturas de interface do usuário do Windows a partir do Windows 10 1703:
Enquadramento / Tecnologia | Suporte | Versão do SO | Dimensionamento de DPI manipulado por | Leitura adicional |
---|---|---|---|---|
Plataforma Universal do Windows (UWP) | Completo | 1607 | Estrutura da interface do usuário | da Plataforma Universal do Windows (UWP) |
Raw Win32/Controles comuns V6 (comctl32.dll) |
|
1703 | Aplicação | de exemplo do GitHub |
Formulários do Windows | Dimensionamento automático limitado de DPI por monitor para alguns controles | 1703 | Estrutura da interface do usuário | Suporte a DPI alto no Windows Forms |
Estrutura de Apresentação do Windows (WPF) | Os aplicativos WPF nativos serão dimensionados pelo DPI, o WPF hospedado em outras estruturas e outras estruturas hospedadas no WPF não serão dimensionadas automaticamente | 1607 | Estrutura da interface do usuário | de exemplo do GitHub |
GDI | Nenhum | N/A | Aplicação | Consulte GDI High-DPI Scaling |
GDI+ | Nenhum | N/A | Aplicação | Consulte GDI High-DPI Scaling |
MFC | Nenhum | N/A | Aplicação | N/A |
Atualizando aplicativos existentes
Para atualizar um aplicativo de desktop existente para lidar com o dimensionamento de DPI corretamente, ele precisa ser atualizado de modo que, no mínimo, as partes importantes de sua interface do usuário sejam atualizadas para responder às alterações de DPI.
A maioria dos aplicativos de desktop é executada no modo de reconhecimento de DPI do sistema. Os aplicativos com reconhecimento de DPI do sistema normalmente são dimensionados para o DPI da tela principal (a tela na qual a bandeja do sistema estava localizada no momento em que a sessão do Windows foi iniciada). Quando o DPI é alterado, o Windows amplia a interface do usuário desses aplicativos, o que geralmente resulta em desfocagem deles. Ao atualizar um aplicativo com reconhecimento de DPI do sistema para se tornar compatível com DPI por monitor, o código que manipula o layout da interface do usuário precisa ser atualizado para que seja executado não apenas durante a inicialização do aplicativo, mas também sempre que uma notificação de alteração de DPI (WM_DPICHANGED no caso do Win32) é recebida. Isso geralmente envolve revisitar quaisquer suposições no código de que a interface do usuário só precisa ser dimensionada uma vez.
Além disso, no caso da programação do Win32, muitas APIs do Win32 não têm nenhum DPI ou contexto de exibição, portanto, elas só retornarão valores relativos ao DPI do sistema. Pode ser útil percorrer seu código para procurar algumas dessas APIs e substituí-las por variantes com reconhecimento de DPI. Algumas das APIs comuns que têm variantes com reconhecimento de DPI são:
Versão DPI única | Versão Per-Monitor |
---|---|
GetSystemMetrics | GetSystemMetricsForDpi |
AdjustWindowRectEx | AdjustWindowRectExForDpi |
SystemParametersInfo | SystemParametersInfoForDpi |
GetDpiForMonitor | GetDpiForWindow |
Também é uma boa ideia procurar tamanhos codificados em sua base de código que assumam um DPI constante, substituindo-os por um código que contabilize corretamente o dimensionamento de DPI. Abaixo está um exemplo que incorpora todas essas sugestões:
Exemplo:
O exemplo abaixo mostra um caso Win32 simplificado de criação de um filho HWND. A chamada para CreateWindow pressupõe que o aplicativo está sendo executado a 96 DPI (USER_DEFAULT_SCREEN_DPI
constante), e nem o tamanho nem a posição do botão estarão corretos em DPIs mais altos:
case WM_CREATE:
{
// Add a button
HWND hWndChild = CreateWindow(L"BUTTON", L"Click Me",
WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
50,
50,
100,
50,
hWnd, (HMENU)NULL, NULL, NULL);
}
O código atualizado abaixo mostra:
- O código de criação de janela DPI dimensionando a posição e o tamanho do HWND filho para o DPI de sua janela pai
- Respondendo à mudança de DPI reposicionando e redimensionando o HWND da criança
- Tamanhos codificados removidos e substituídos por código que responde a alterações de DPI
#define INITIALX_96DPI 50
#define INITIALY_96DPI 50
#define INITIALWIDTH_96DPI 100
#define INITIALHEIGHT_96DPI 50
// DPI scale the position and size of the button control
void UpdateButtonLayoutForDpi(HWND hWnd)
{
int iDpi = GetDpiForWindow(hWnd);
int dpiScaledX = MulDiv(INITIALX_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI);
int dpiScaledY = MulDiv(INITIALY_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI);
int dpiScaledWidth = MulDiv(INITIALWIDTH_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI);
int dpiScaledHeight = MulDiv(INITIALHEIGHT_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI);
SetWindowPos(hWnd, hWnd, dpiScaledX, dpiScaledY, dpiScaledWidth, dpiScaledHeight, SWP_NOZORDER | SWP_NOACTIVATE);
}
...
case WM_CREATE:
{
// Add a button
HWND hWndChild = CreateWindow(L"BUTTON", L"Click Me",
WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
0,
0,
0,
0,
hWnd, (HMENU)NULL, NULL, NULL);
if (hWndChild != NULL)
{
UpdateButtonLayoutForDpi(hWndChild);
}
}
break;
case WM_DPICHANGED:
{
// Find the button and resize it
HWND hWndButton = FindWindowEx(hWnd, NULL, NULL, NULL);
if (hWndButton != NULL)
{
UpdateButtonLayoutForDpi(hWndButton);
}
}
break;
Ao atualizar um aplicativo com reconhecimento de DPI do sistema, algumas etapas comuns a serem seguidas são:
- Marque o processo como com reconhecimento de DPI por monitor (V2) usando um manifesto do aplicativo (ou outro método, dependendo da(s) estrutura(s) da interface do usuário usada(s).
- Torne a lógica de layout da interface do usuário reutilizável e mova-a para fora do código de inicialização do aplicativo de modo que possa ser reutilizada quando ocorrer uma alteração de DPI (WM_DPICHANGED no caso da programação do Windows (Win32).
- Invalide qualquer código que assuma que os dados sensíveis ao DPI (DPI/fontes/tamanhos/etc.) nunca precisam ser atualizados. É uma prática muito comum armazenar em cache tamanhos de fonte e valores de DPI na inicialização do processo. Ao atualizar um aplicativo para obter reconhecimento de DPI por monitor, os dados sensíveis ao DPI devem ser reavaliados sempre que um novo DPI for encontrado.
- Quando ocorrer uma alteração de DPI, recarregue (ou rasterize) todos os ativos de bitmap para o novo DPI ou, opcionalmente, estenda os ativos carregados atualmente para o tamanho correto.
- Grep para APIs que não reconhecem DPI Per-Monitor e as substitua por APIs com reconhecimento de DPI Per-Monitor (quando aplicável). Exemplo: substitua GetSystemMetrics por GetSystemMetricsForDpi.
- Teste seu aplicativo em um sistema de vários monitores/vários DPI.
- Para quaisquer janelas de nível superior em seu aplicativo que você não possa atualizar para a escala de DPI corretamente, use o dimensionamento de DPI de modo misto (descrito abaixo) para permitir o alongamento de bitmap dessas janelas de nível superior pelo sistema.
Mixed-Mode DPI Scaling (Sub-Process DPI Scaling)
Ao atualizar um aplicativo para oferecer suporte ao reconhecimento de DPI por monitor, às vezes pode se tornar impraticável ou impossível atualizar todas as janelas do aplicativo de uma só vez. Isso pode ser simplesmente devido ao tempo e esforço necessários para atualizar e testar toda a interface do usuário, ou porque você não possui todo o código da interface do usuário que precisa executar (se seu aplicativo talvez carregue a interface do usuário de terceiros). Nessas situações, o Windows oferece uma maneira de entrar facilmente no mundo do reconhecimento por monitor, permitindo que você execute algumas das janelas do aplicativo (apenas de nível superior) no modo de reconhecimento de DPI original enquanto concentra seu tempo e energia atualizando as partes mais importantes da interface do usuário.
Abaixo está uma ilustração de como isso poderia parecer: você atualiza a interface do usuário do aplicativo principal ("Janela principal" na ilustração) para ser executada com reconhecimento de DPI por monitor enquanto executa outras janelas no modo existente ("Janela secundária").
Antes da Atualização de Aniversário do Windows 10 (1607), o modo de reconhecimento de DPI de um processo era uma propriedade de todo o processo. A partir da Atualização de Aniversário do Windows 10, essa propriedade agora pode ser definida por janela de de nível superior. (As janelas de filho devem continuar a corresponder ao tamanho de escala dos pais.) Uma janela de nível superior é definida como uma janela sem pai. Esta é normalmente uma janela "regular" com botões minimizar, maximizar e fechar. O cenário para o qual o reconhecimento de DPI de subprocesso se destina é ter a interface do usuário secundária dimensionada pelo Windows (bitmap esticado) enquanto você concentra seu tempo e recursos na atualização da interface do usuário principal.
Para habilitar o reconhecimento de DPI de subprocesso, chame SetThreadDpiAwarenessContext antes e depois de qualquer chamada de criação de janela. A janela criada será associada ao reconhecimento de DPI definido por meio de SetThreadDpiAwarenessContext. Use a segunda chamada para restaurar o reconhecimento de DPI do thread atual.
Embora o uso do dimensionamento de DPI de subprocesso permita que você confie no Windows para fazer parte do dimensionamento de DPI para seu aplicativo, ele pode aumentar a complexidade do seu aplicativo. É importante que compreenda as desvantagens desta abordagem e a natureza das complexidades que introduz. Para obter mais informações sobre reconhecimento de DPI de subprocesso, consulte Mixed-Mode DPI Scaling e DPI-aware APIs.
Testando suas alterações
Depois de atualizar seu aplicativo para se tornar ciente de DPI por monitor, é importante validar que seu aplicativo responde corretamente às alterações de DPI em um ambiente de DPI misto. Algumas especificidades a testar incluem:
- Movendo janelas de aplicativos para frente e para trás entre exibições de diferentes valores de DPI
- Iniciando seu aplicativo em exibições de diferentes valores de DPI
- Alterando o fator de escala do monitor enquanto o aplicativo está em execução
- Alterar o ecrã que utiliza como ecrã principal terminar sessão no Windowse, em seguida, voltar a testar a aplicação depois de iniciar sessão novamente. Isso é particularmente útil para encontrar código que usa tamanhos/dimensões codificados.
Armadilhas comuns (Win32)
Não utilizar o retângulo sugerido que é fornecido no WM_DPICHANGED
Quando o Windows envia uma mensagem WM_DPICHANGED janela do aplicativo, essa mensagem inclui um retângulo sugerido que você deve usar para redimensionar a janela. É fundamental que seu aplicativo use esse retângulo para se redimensionar, pois isso irá:
- Certifique-se de que o cursor do mouse permanecerá na mesma posição relativa na janela ao arrastar entre monitores
- Impeça que a janela do aplicativo entre em um ciclo recursivo de alteração de DPI em que uma alteração de DPI aciona uma alteração de DPI subsequente, que dispara outra alteração de DPI.
Se você tiver requisitos específicos do aplicativo que o impeçam de usar o retângulo sugerido que o Windows fornece na mensagem WM_DPICHANGED, consulte WM_GETDPISCALEDSIZE. Esta mensagem pode ser usada para dar ao Windows o tamanho desejado que você gostaria de usar depois que a alteração de DPI ocorreu, evitando os problemas descritos acima.
Falta de documentação sobre virtualização
Quando um HWND ou processo está sendo executado como DPI sem reconhecimento ou DPI do sistema, ele pode ser estendido pelo Windows. Quando isso acontece, o Windows dimensiona e converte informações confidenciais de DPI de algumas APIs para o espaço de coordenadas do thread de chamada. Por exemplo, se um thread sem reconhecimento de DPI consultar o tamanho da tela durante a execução em uma tela de alto DPI, o Windows virtualizará a resposta dada ao aplicativo como se a tela estivesse em unidades de 96 DPI. Como alternativa, quando um thread com reconhecimento de DPI do sistema estiver interagindo com uma exibição em um DPI diferente do que estava em uso quando a sessão do usuário atual foi iniciada, o Windows dimensionará algumas chamadas de API para o espaço de coordenadas que o HWND usaria se estivesse sendo executado em seu fator de escala DPI original.
Quando você atualiza seu aplicativo de desktop para a escala de DPI corretamente, pode ser difícil saber quais chamadas de API podem retornar valores virtualizados com base no contexto do thread; essas informações não estão atualmente suficientemente documentadas pela Microsoft. Lembre-se de que, se você chamar qualquer API do sistema a partir de um contexto de thread sem reconhecimento de DPI ou com reconhecimento de DPI do sistema, o valor de retorno poderá ser virtualizado. Como tal, certifique-se de que o thread está a ser executado no contexto DPI esperado ao interagir com o ecrã ou janelas individuais. Ao alterar temporariamente o contexto DPI de um thread usando SetThreadDpiAwarenessContext, certifique-se de restaurar o contexto antigo quando terminar para evitar causar um comportamento incorreto em outro lugar do aplicativo.
Muitas APIs do Windows não têm um contexto DPI
Muitas APIs herdadas do Windows não incluem um contexto DPI ou HWND como parte de sua interface. Como resultado, os desenvolvedores geralmente precisam fazer trabalho adicional para lidar com o dimensionamento de qualquer informação sensível ao DPI, como tamanhos, pontos ou ícones. Por exemplo, os desenvolvedores que usam LoadIcon devem ampliar ícones carregados de bitmap ou usar APIs alternativas para carregar ícones de tamanho correto para o DPI apropriado, como LoadImage.
Redefinição forçada do de reconhecimento de DPI em todo o processo
Em geral, o modo de reconhecimento de DPI do seu processo não pode ser alterado após a inicialização do processo. O Windows pode, no entanto, alterar à força o modo de reconhecimento de DPI do seu processo se você tentar quebrar o requisito de que todos os HWNDs em uma árvore de janela tenham o mesmo modo de reconhecimento de DPI. Em todas as versões do Windows, a partir do Windows 10 1703, não é possível ter diferentes HWNDs em uma árvore HWND executados em diferentes modos de reconhecimento de DPI. Se você tentar criar uma relação filho-pai que quebre essa regra, o reconhecimento de DPI de todo o processo poderá ser redefinido. Isso pode ser desencadeado por:
- Uma chamada CreateWindow em que a janela pai passada é de um modo de reconhecimento de DPI diferente do thread de chamada.
- Uma chamada SetParent onde as duas janelas estão associadas a diferentes modos de reconhecimento de DPI.
A tabela abaixo mostra o que acontece se você tentar violar essa regra:
Funcionamento | Windows 8.1 | Windows 10 (1607 e anteriores) | Windows 10 (1703 e posterior) |
---|---|---|---|
CreateWindow (In-Proc) | N/A | Filho herda (modo misto) | Filho herda (modo misto) |
CreateWindow (Cross-Proc) | Redefinição forçada (do processo do chamador) | Filho herda (modo misto) | Redefinição forçada (do processo do chamador) |
SetParent (In-Proc) | N/A | Redefinição forçada (do processo atual) | Falha (ERROR_INVALID_STATE) |
SetParent (Cross-Proc) | Redefinição forçada (do processo da janela filho) | Redefinição forçada (do processo da janela filho) | Redefinição forçada (do processo da janela filho) |
Tópicos relacionados
Mixed-Mode Escalabilidade de DPI e APIs com reconhecimento de DPI.