Compartir a través de


Este artículo proviene de un motor de traducción automática.

Windows con C++

Representación en una aplicación de escritorio con Direct2D

Kenny Kerr

 

Kenny KerrEn mi última columna, veíamos cómo realmente es fácil crear una aplicación de escritorio con C++ sin marco ni biblioteca. De hecho, si usted se siente particularmente masoquista, podría escribir una aplicación de escritorio toda desde dentro de su función WinMain como lo he hecho figura 1. Por supuesto, ese enfoque simplemente no escala.

 

 

Figura 1 el masoquista ventana

int __stdcall wWinMain(HINSTANCE module, HINSTANCE, PWSTR, int)
{
  WNDCLASS wc = {};
  wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
  wc.hInstance = module;
  wc.lpszClassName = L"window";
  wc.lpfnWndProc = [] (HWND window, UINT message, WPARAM
    wparam, LPARAM lparam) -> LRESULT
  {
    if (WM_DESTROY == message)
    {
      PostQuitMessage(0);
      return 0;
    }
    return DefWindowProc(window, message, wparam, lparam);
  };
  RegisterClass(&wc);
  CreateWindow(wc.lpszClassName, L"Awesome?!",
    WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, module, nullptr);
  MSG message;
  BOOL result;
  while (result = GetMessage(&message, 0, 0, 0))
  {
    if (-1 != result) DispatchMessage(&message);
  }
}

También mostré cómo el Active Template Library (ATL) proporciona una buena abstracción C++ para ocultar gran parte de esta maquinaria y cómo la biblioteca de plantillas de Windows (WTL) toma esto aún más, sobre todo para aplicaciones fuertemente invertido en los usuario y GDI enfoques para el desarrollo de la aplicación (ver mi columna de febrero msdn.microsoft.com/magazine/jj891018).

El futuro de la representación de la aplicación en Windows es hardware -­aceleración Direct3D, pero que realmente no es práctico trabajar con directamente si todo lo que quieras hacer es hacer una solicitud de dos dimensiones o juego. Es donde entra en juego Direct2D. Introduje Direct2D brevemente cuando primero se anunció hace unos años, pero voy a dedicar los próximos meses acercándonos mucho en el desarrollo de Direct2D. Echale un vistazo mi columna de junio de 2009, "Introducción de Direct2D" (msdn.microsoft.com/magazine/dd861344), una introducción a la arquitectura y los fundamentos de Direct2D.

Uno de los fundamentos de diseño clave de Direct2D es que se centra en la representación y deja los otros aspectos de desarrollo de aplicaciones de Windows a usted o a otras bibliotecas que se pueden emplear. Aunque Direct2D fue diseñado para representar en una ventana de escritorio, incumbe a usted realmente esta ventana y optimizarlo para la representación de Direct2D. Para que este mes, yo me voy a centrar en la relación única entre Direct2D y la ventana de la aplicación de escritorio. Usted puede hacer muchas cosas para optimizar la ventana manejo y proceso de renderizado. Desea reducir pintura innecesario y evitar el parpadeo y a proporcionar la mejor experiencia posible para el usuario. Por supuesto, también querrá darle un marco manejable en el cual desarrollar su aplicación. Yo voy a abordar estos temas aquí.

La ventana del escritorio

En el ejemplo ATL el mes pasado, dio el ejemplo de una clase de ventana derivada de la plantilla de clase ATL CWindowImpl. Todo bien está contenido dentro de la clase de ventana de la aplicación. Sin embargo, lo que termina sucediendo es un montón de ventana y renderizado plomería termina entremezclado con representación específica de la aplicación y control de eventos de la ventana. Para solucionar este problema, me inclino a empujar tanto de este código repetitivo posible en una clase base, usando polimorfismo de tiempo de compilación para llegar hasta la clase de ventana de la aplicación cuando esta clase base necesita su atención. Este enfoque se utiliza mucho por ATL y WTL, así que ¿por qué no extender por sus propias clases?

Figura 2 ilustra esta separación. La clase base es el escritorio­plantilla de clase de ventana. El parámetro de plantilla proporciona la clase base la capacidad para llamar a la clase concreta sin el uso de funciones virtuales. En este caso, utiliza esta técnica para ocultar un montón de procesamiento específico anterior y posterior al llamar a la ventana de la aplicación para realizar las operaciones de dibujo actuales. Voy ampliar la plantilla de la clase DesktopWindow en un momento, pero primero su registro de clase de ventana necesita un poco de trabajo.

Figura 2 la ventana del escritorio

template <typename T>
class DesktopWindow :
  public CWindowImpl<DesktopWindow<T>, CWindow,
    CWinTraits<WS_OVERLAPPEDWINDOW | WS_VISIBLE>>
{
  BEGIN_MSG_MAP(DesktopWindow)
    MESSAGE_HANDLER(WM_PAINT, PaintHandler)
    MESSAGE_HANDLER(WM_DESTROY, DestroyHandler)
  END_MSG_MAP()
  LRESULT DestroyHandler(UINT, WPARAM, LPARAM, BOOL &)
  {
    PostQuitMessage(0);
    return 0;
  }
  LRESULT PaintHandler(UINT, WPARAM, LPARAM, BOOL &)
  {
    PAINTSTRUCT ps;
    VERIFY(BeginPaint(&ps));
    Render();
    EndPaint(&ps);
    return 0;
  }
  void Render()
  {
    ...
          static_cast<T *>(this)->Draw();
    ...
          }
    ...
          };
struct SampleWindow : DesktopWindow<SampleWindow>
{
  void Draw()
  {
    ...
          }
};

Optimización de la clase de ventana

Una de las realidades de la API de Windows para aplicaciones de escritorio es que fue diseñado para simplificar la representación con los recursos tradicionales de usuario y GDI . Algunas de estas "comodidades" deben ser desactivado para permitir Direct2D para asumir el control, para evitar innecesaria pintura hacia feo parpadeo. Otro de estos defectos también debe ser ajustado para trabajar de una manera que mejor se adapte a representación de Direct2D. Mucho de esto se logra al cambiar la información de la clase de ventana antes de que esté registrada, pero usted puede haber notado que ATL esto oculta del programador. Afortunadamente, todavía hay una forma de lograrlo.

El mes pasado, me mostró cómo la API de Windows espera una estructura de clase de ventana para registrar antes de una ventana se crea en base a su especificación. Uno de los atributos de una clase de ventana es su pincel de fondo. Windows utiliza este cepillo GDI para despejar el área de cliente de la ventana antes de que la ventana empieza a pintar. Esto era conveniente en los días de usuario y GDI pero es innecesario y la causa de algunos parpadeo para aplicaciones de Direct2D. Una manera sencilla de evitar esto es colocando el mango del cepillo de fondo en la estructura de clase de ventana a un nullptr. Si desarrollas en un equipo Windows 7 o Windows 8 relativamente rápido, se podría pensar que esto es innecesario porque no observa ningún parpadeo. Eso es sólo porque el moderno escritorio de Windows se compone tan eficientemente en los gráficos de procesamiento unidad (GPU) que es difícil recogerlo. Sin embargo, es bastante fácil para la canalización de procesamiento para exagerar el efecto que pueden producirse en máquinas más lentas. Si el cepillo de fondo de la ventana es blanco y luego cliente área con un contraste Negro cepillo de la ventana de pintura, añadiendo un pequeño retardo — en cualquier parte entre 10 ms y ms 100 — no tendrás ningún problema en recoger el pulso del parpadeo. ¿Así cómo evitarlo?

Como mencioné, si su registro de clase de ventana carece de un cepillo de fondo, entonces Windows no tendrá ningún cepillo con el que limpiar su ventana. Sin embargo, usted puede haber notado en los ejemplos ATL que el registro de clase de ventana se oculta totalmente. Una solución común es manejar el mensaje WM_ERASEBKGND que por defecto manijas de procesamiento de la ventana, cortesía de la función DefWindowProc — por el área de cliente de la ventana con el cepillo de fondo de clase de ventana de la pintura. Si manejas este mensaje, devolver true, entonces se produce ninguna pintura. Esta es una razón­capaz de solución, como este mensaje se envía a una ventana independientemente de si la clase de ventana tiene un cepillo de fondo válido o no. Otra solución es simplemente evitar este controlador no-op y extraiga el contacto de fondo de la clase de ventana en primer lugar. Afortunadamente, ATL hace relativamente simple anular esta parte de la creación de la ventana. Durante la creación, ATL llama al método de GetWndClassInfo en la ventana para obtener esta información de la clase de ventana. Usted puede proporcionar su propia implementación de este método, pero ATL proporciona una macro práctica que implementa para usted:

DECLARE_WND_CLASS_EX(nullptr, CS_HREDRAW | CS_VREDRAW, -1);

El último argumento para esta macro está destinado a ser una constante del cepillo, pero el valor de-1 lo engaña claro este atributo de la estructura de clase de ventana. Una manera segura de determinar si ha sido borrado el fondo de la ventana es verificar la PAINTSTRUCT por la función de BeginPaint en el controlador WM_PAINT. Si sus miembros fErase es false, entonces sabrás que Windows haya desaparecido fondo de la ventana, o al menos que algunos cifran respondió al mensaje WM_ERASEBKGND y pretendió quitarlo. Si el controlador de mensajes WM_ERASEBKGND no o no es capaz de eliminar el fondo, entonces incumbe al controlador de mensaje WM_PAINT para hacerlo. Sin embargo, aquí podemos emplear Direct2D completamente asumir la representación del área de cliente de la ventana y evitar este cuadro doble. Sólo asegúrese de llamar a la función EndPaint, asegurando de Windows que se hecho pintar su ventana, de lo contrario Windows seguirá le acosar con mensajes innecesarios corriente de WM_PAINT. Esto, por supuesto, rendimiento de la aplicación y aumentar la potencia total de consumo.

El otro aspecto de la información de clase de ventana que merece nuestra atención es los estilos de clase de ventana. Esto es, de hecho, qué es el segundo argumento a la macro anterior para. Los estilos CS_HREDRAW y CS_VREDRAW hacer que la ventana se pierde cada vez que la ventana se redimensiona tanto vertical como horizontalmente. Esto ciertamente no es necesario. Usted podría, por ejemplo, manejar el mensaje WM_SIZE e invalida la ventana allí, pero siempre me alegro cuando Windows me salvará de escribir unas líneas adicionales de código. De cualquier manera, si descuidas invalidar la ventana, luego Windows no enviaremos su ventana cualquier mensaje WM_PAINT cuando se reduce el tamaño de la ventana. Esto podría estar bien si eres feliz para el contenido de la ventana se aparte, pero es común estos días a pintar diversos activos de ventana con respecto al tamaño de la ventana. Lo que usted prefiere, se trata de una decisión explícita que necesita hacer para su ventana de la aplicación.

Mientras estoy en el tema de fondo del ventana, a menudo es deseable para invalidar una ventana explícitamente. Esto permite mantener la representación de su ventana en el mensaje WM_PAINT antes que tener que manejar la pintura en diversos lugares y en rutas de código diferente a través de su aplicación. Puede pintar algo en respuesta a un clic de ratón. Usted podría, por supuesto, hacer la representación justo allí en el controlador de mensajes. Alternativamente, usted simplemente podría invalidar la ventana y dejó el controlador WM_PAINT representar el estado actual de la aplicación. Éste es el papel de la función InvalidateRect. ATL proporciona el método Invalidate que envuelve a esta función. Lo que a menudo confunde los desarrolladores acerca de esta función es cómo lidiar con el parámetro de "borrar". La sabiduría parece que decir "sí" para borrar hará que la ventana para volver a dibujar inmediatamente y decir "no" se aplazar esto de alguna manera. Esto no es cierto y la documentación dice tanto. Invalidar una ventana hará que ser repintado con prontitud. La opción de borrar es reemplaza la función DefWindowProc, que normalmente sería claro el fondo de la ventana. Si borrar es verdadera, entonces la siguiente llamada al BeginPaint borrará el fondo de la ventana. Aquí, entonces, es otra razón para evitar el pincel de fondo de clase de ventana totalmente en lugar de depender de un controlador de mensajes WM_ERASEBKGND. Sin cepillo fondo, BeginPaint otra vez tiene nada para pintar con, por lo que la opción de borrar no tiene ningún efecto. Si dejas ATL establece un pincel de fondo para su clase de ventana, entonces debes ser cuidadoso al invalidar su ventana porque esto otra vez introducirá el parpadeo. He añadido a este miembro protegido a la plantilla de la clase DesktopWindow para ello:

void Invalidate()
{
  VERIFY(InvalidateRect(nullptr, false));
}

También es una buena idea para manejar el mensaje WM_DISPLAYCHANGE para invalidar la ventana. Esto asegura que la ventana se repinta correctamente debe algo sobre la pantalla cambio afectan la apariencia de la ventana.

Ejecutando la aplicación

Me gusta mantener la función WinMain de mi aplicación relativamente simple. Para lograr este objetivo, he añadido un método de gestión público a la plantilla de la clase DesktopWindow para ocultar la ventana entera y creación de fábrica de Direct2D, así como el bucle de mensajes. El DesktopWindow la carrera del método se muestra en figura 3. Esto me permite escribir la función WinMain de mi aplicación sencillamente:

int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
  SampleWindow window;
  return window.Run();
}

Figura 3 el método Run DesktopWindow

int Run()
{
  D2D1_FACTORY_OPTIONS fo = {};
  #ifdef DEBUG
  fo.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
  #endif
  HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
                       fo,
                       m_factory.GetAddressOf()));
  static_cast<T *>(this)->CreateDeviceIndependentResources();
  VERIFY(__super::Create(nullptr, nullptr, L"Direct2D"));
  MSG message;
  BOOL result;
  while (result = GetMessage(&message, 0, 0, 0))
  {
    if (-1 != result)
    {
      DispatchMessage(&message);
    }
  }
  return static_cast<int>(message.wParam);
}

Antes de crear la ventana, preparo las opciones de fábrica de Direct2D activando la capa de depuración para depuración construye. Recomiendo que hagas lo mismo, ya que permite Direct2D trazar todas las clases de diagnóstico útil al desarrollar su aplicación. La función D2D1CreateFactory devuelve el puntero de interfaz de fábrica que entregar a excelente ComPtr puntero inteligente de la biblioteca tiempo de ejecución de Windows, un miembro protegido de la clase DesktopWindow. Luego llamar al método CreateDeviceIndependentResources para crear recursos independientes del dispositivo — cosas como geometrías y estilos de trazo que pueden reutilizarse durante toda la vida de la aplicación. Aunque permiten que las clases derivadas reemplazar este método, proporciono un trozo vacío en la plantilla de la clase de DesktopWindow si esto no es necesario. Por último, el método Run concluye bloqueando con un bucle de mensajes simples. Echale un vistazo y empieza la columna del mes pasado para una explicación sobre el bucle de mensajes.

El objetivo de Render

El objetivo de procesar Direct2D debe crearse bajo demanda dentro del método Render llamado como parte del manejador de mensaje WM_PAINT. A diferencia de algunos de los otros objetivos de procesamiento de Direct2D, es totalmente posible que el dispositivo — una GPU en la mayoría de los casos — que provee el procesamiento acelerado por hardware para una ventana de escritorio puede desaparecer o cambiar de alguna manera como para hacer cualquier recursos asignados por el destino de procesamiento no válido. Debido a la naturaleza de la representación de modo inmediato en Direct2D, la aplicación es responsable de seguimiento qué recursos son específicos de cada dispositivo y deben ser recreado de vez en cuando. Afortunadamente, esto es bastante fácil de administrar. Figura 4 proporciona el método Render de la DesktopWindow completo.

Figura 4 el método Render de la DesktopWindow

void Render()
{
  if (!m_target)
  {
    RECT rect;
    VERIFY(GetClientRect(&rect));
    auto size = SizeU(rect.right, rect.bottom);
    HR(m_factory->CreateHwndRenderTarget(RenderTargetProperties(),
      HwndRenderTargetProperties(m_hWnd, size),
      m_target.GetAddressOf()));
    static_cast<T *>(this)->CreateDeviceResources();
  }
  if (!(D2D1_WINDOW_STATE_OCCLUDED & m_target->CheckWindowState()))
  {
    m_target->BeginDraw();
    static_cast<T *>(this)->Draw();
    if (D2DERR_RECREATE_TARGET == m_target->EndDraw())
    {
      m_target.Reset();
    }
  }
}

El método Render comienza comprobando si la ComPtr dirigiendo la interfaz COM de procesamiento del objetivo es válido. De esta manera, sólo recrea el objetivo de procesar cuando sea necesario. Esto sucederá al menos una vez la primera vez que se procesa la ventana. Si algo le pasa al dispositivo subyacente, o por alguna razón el destino de procesamiento deben ser recreados, entonces el método de EndDraw hacia el final del método Render en figura 4 devolverá la constante D2DERR_RECREATE_TARGET. El método ComPtr Reset entonces utiliza para lanzar simplemente el objetivo de procesar. La próxima vez que la ventana se pregunta a sí mismo, pintar el Render método pasará por las propuestas de creación de un nuevo Direct2D hacer blanco.

Comienza por llegar al área de cliente de la ventana en píxeles físicas. Direct2D en su mayor parte utiliza píxeles lógicas exclusivamente para permitirle apoyar alta DPI pantallas naturalmente. Este es el punto en que inicia la relación entre la pantalla física y su sistema de coordenadas lógico. A continuación, llama la fábrica de Direct2D para crear el objeto de destino de render. Es en este punto que llama a la clase de ventana de derivadas de la aplicación para crear recursos específicos — cosas como pinceles y mapas de bits que son dependientes del dispositivo subyacente al objetivo de procesamiento. Una vez más, un trozo vacío es proporcionada por la clase de DesktopWindow si esto no es necesario.

Antes de dibujar, el método Render comprueba que la ventana es visible y no totalmente obstruida. Esto evita cualquier procesamiento innecesario. Por lo general, esto sólo ocurre cuando la cadena de intercambio subyacente de DirectX es invisible, como cuando el usuario bloquea o cambia de ordenadores de sobremesa. Los métodos BeginDraw y EndDraw straddle entonces la llamada al método de dibujo de la ventana de la aplicación. Direct2D aprovecha la ocasión para lotes de geometrías en un búfer de vértice, se aglutinan dibujo comandos, y así sucesivamente para proporcionar el mayor rendimiento y performance en la GPU.

El último paso crítico para integrar Direct2D correctamente con una ventana de escritorio es redimensionar el objetivo de procesar cuando se cambia el tamaño de la ventana. Ya he hablado de cómo la ventana queda automáticamente invalidada para que se repinta con prontitud, sino el objetivo de procesar sí mismo no tiene ninguna idea que han cambiado la dimensiones de la ventana. Afortunadamente, esto es bastante fácil de hacer, como figura 5 ilustra.

Figura 5 redimensionar el objetivo de Render

MESSAGE_HANDLER(WM_SIZE, SizeHandler)
LRESULT SizeHandler(UINT, WPARAM, LPARAM lparam, BOOL &)
{
  if (m_target)
  {
    if (S_OK != m_target->Resize(SizeU(LOWORD(lparam),
      HIWORD(lparam))))
    {
      m_target.Reset();
    }
  }
  return 0;
}

Asumiendo que la ComPtr actualmente tiene un puntero de interfaz de destino COM válido render, método de cambiar el tamaño del objetivo render se llama con el nuevo tamaño, conforme a lo dispuesto por LPARAM del mensaje ventana. Si por alguna razón que el objetivo de procesar no puede cambiar el tamaño de todos los recursos internos, entonces el ComPtr simplemente se reinicia, obligando a la meta de procesamiento se vuelve a crear la siguiente representación de tiempo se solicita.

Y eso es todo lo que tengo espacio para en la columna de este mes. Ahora tienes todo lo que necesita tanto crear y administrar una ventana de escritorio así como a utilizar la GPU para representar en la ventana de la aplicación. A mí el mes próximo al continuar explorar Direct2D.

Kenny Kerr es un programador de computadoras con sede en Canadá, un autor de Pluralsight y un MVP de Microsoft. Blog en kennykerr.ca y lo puedes seguir en Twitter en twitter.com/kennykerr.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Worachai Chaoweeraprasit