Ir al contenido


Foto

[DELPHI] Programación Windows con API pura


  • Por favor identifícate para responder
9 respuestas en este tema

#1 escafandra

escafandra

    Advanced Member

  • Administrador
  • 4.107 mensajes
  • LocationMadrid - España

Escrito 30 mayo 2013 - 06:10

Este tutorial lo voy a enfocar a la programación Windows a bajo nivel con API pura. Es un tema un tanto árido pero pienso que si se entiende puede llegar a ser estimulante entender y ser capaz de adentrarnos en esta parte un tanto desconocida para muchos.

Con el fin de amenizar este, ya de por si, árido tema voy a escribir con vosotros una aplicación salvapantallas en delphi que será totalmente funcional y útil.

El bucle de mensajes:
En primer lugar ya sabemos que Windows se maneja a base de mensajes y que lo hace a través de un bucle de mensajes. Esos mensajes son tratados en una función especial de tratamiento de mensajes. Cuando creamos una ventana tenemos de definir una de las clases preexistentes o una nueva definida por usuario.

Todo bucle de mensajes en Windows tiene mas o menos este aspecto:

delphi
  1. while(GetMessage(Msg, 0, 0, 0)) do
  2. begin
  3.   TranslateMessage(Msg);
  4.   DispatchMessage(Msg);
  5. end;

GetMessage: Espera hasta recibir un mensaje entrante.
TranslateMessage: Traduce los mensajes del teclado de Virtual-Key a caracteres produciendo un mensaje tipo WM_CHAR o WM_DEADCHAR.
DispatchMessage: envía el mensaje a la función de tratamiento de mensajes de la ventana destinataria.

En nuestro primer proyecto vamos a crear una ventana que ocupe toda la pantalla y la vamos a pintar de un solo color tratando el mensaje WM_PAINT  y la vamos a destruir con WM_DESTROY.

Algunos habréis visto algo de código de ejemplo en las páginas de ayuda de Microsoft y os resultará familiar una función principal como esta:



delphi
  1. function WinMain(hInstance, hPrevInstance: THISTANCE; lpCmdLine: LPSTR; nCmdShow: integer): cardinal; stdcall;

En Delphi esta función no está predefinida en ningún proyecto, así que para poder acercar nuestro código de ejemplo a lo que podáis ver en la web, nos la vamos a currar. El siguiente código va a ser común en todos los ejemplos y constituye el inicio de nuestro programa:

delphi
  1. program BanScr_1;
  2.  
  3. uses
  4.   Windows,
  5.   Unit1 in 'Unit1.pas';
  6.  
  7. var
  8.   info: STARTUPINFO;
  9.   CmdLine: PCHAR;
  10.   n: integer;
  11.  
  12. begin
  13.   GetStartupInfo(info);
  14.   CmdLine:= GetCommandLine();
  15.   n:=0;
  16.   //Navegamos por CmdLine a través de comillas y espacios
  17.   //para aislar solo los parámetros pasados a nuestra App.
  18.   if CmdLine^ = #34 then
  19.     repeat inc(n);
  20.     until(CmdLine[n] = #34);
  21.   while(CmdLine[n]<>#32) do inc(n);
  22.   inc(n);
  23.   if n = lstrlen(CmdLine) then CmdLine:= nil
  24.   else CmdLine:= CmdLine+n;
  25.   ExitProcess(WinMain(GetModuleHandle(nil), 0, CmdLine, info.wShowWindow));
  26. end.

El código anterior se encarga de crear los parámetros para la función main, hInstance de la aplicación, la línea de comandos que se le pasa (eliminando el nombre del ejecutable y la ruta) y la visibilidad de la misma. Vemos el uso exclusivo de la API incluso para el tratamiento de cadenas de texto. Este proyecto base puede servir para cualquier aplicación Windows a bajo nivel que posteriormente queramos escribir.

La función WinMain y el resto del programa la escribiremos en un módulo aparte: Unit1.pas

delphi
  1. function WinMain(hInstance, hPrevInstance: THISTANCE; lpCmdLine: LPSTR; nCmdShow: integer): cardinal; stdcall;
  2. var
  3.   Wnd: HWND;
  4.   WScreen, HScreen: integer;
  5.   WinClass: WNDCLASS;
  6.   Msg: TMsg;
  7. begin
  8.   WScreen:= GetSystemMetrics(SM_CXSCREEN);
  9.   HScreen:= GetSystemMetrics(SM_CYSCREEN);
  10.   ZeroMemory(@WinClass, sizeof(WinClass));
  11.   WinClass.lpfnWndProc:= @WindowProc;
  12.   WinClass.lpszClassName:= 'WindowsScreenSaverClass';
  13.   // Registramos nuestra clase de ventana
  14.   RegisterClass(WinClass);
  15.   //Creamos la ventana
  16.   Wnd:= CreateWindowEx(0, WinClass.lpszClassName, 'SCR', WS_VISIBLE + WS_OVERLAPPEDWINDOW,
  17.                         0, 0, WScreen, HScreen, HWND_DESKTOP, 0, 0, nil);
  18.   // El bucle de mensajes
  19.   while(GetMessage(Msg, 0, 0, 0)) do
  20.   begin
  21.     TranslateMessage(Msg);
  22.     DispatchMessage(Msg);
  23.   end;
  24.  
  25.   Result:= 0;
  26. end;

En este caso estamos creando una nueva clase de ventana que llamamos SCR y a la que asociamos una función de tratamiento de mensajes WindowProc. En principio creamos una ventana de esa clase que va a ocupar toda la pantalla y va a tener caption y botones de sistema. Esa ventana va a responder a dos mensajes: WM_PAINT y WM_DESTROY. En ella sólo vamos a dibujar un rectángulo blanco que ocupa toda su área cliente.

Función de tratamiento de mensajes:
Esta función tendrá la siguiente forma para ir respondiendo a los diferentes mensajes que le correspondan. Cada ventana estará diseñada para responder a los que nos parezcan.

delphi
  1. function WindowProc(Wnd: HWND; uMsg: Cardinal; wParam, lParam: Integer): Integer; stdcall;
  2. begin
  3.   case uMsg of
  4.     WM_PAINT:
  5.       .
  6.     WM_CHAR:
  7.       ..
  8.       .....
  9.     WM_DESTROY:
  10.       PostQuitMessage(0);                //Destruimos la ventana
  11.  
  12.     else
  13.       // Función por defecto de tratamiento de mensajes.
  14.       Result:= DefWindowProc(Wnd, uMsg, wParam, lParam);  end;
  15.   end; 
  16. end;

Para asociar la función con la ventana tenemos la API RegisterClass Esta API usa como parámetro una estructura WNDCLASS que, entre otros valores, porta un puntero a la función de tratamiento de mensajes.

Este es el código completo:

delphi
  1. unit Unit1;
  2.  
  3. interface
  4.  
  5. uses
  6.   Windows,
  7.   Messages;
  8.  
  9. type THISTANCE = HMODULE;
  10.  
  11. function  WinMain(hInstance, hPrevInstance: HMODULE; lpCmdLine: LPSTR; nCmdShow: integer): cardinal; stdcall;
  12.  
  13. implementation
  14.  
  15. function WindowProc(Wnd: HWND; uMsg: Cardinal; wParam, lParam: Integer): Integer; stdcall;
  16. var
  17.   ps: PAINTSTRUCT;
  18.   DC: HDC;
  19.   Brush: HBRUSH;
  20.   Rect: TRect;
  21. begin
  22.   Result := 0;
  23.   case uMsg of
  24.     WM_PAINT:
  25.     begin
  26.       DC:= BeginPaint(Wnd, ps);          //Comenzamos
  27.       GetWindowRect(Wnd, Rect);
  28.       Brush:= CreateSolidBrush($FFFFFF); //brocha blanca
  29.       SelectObject(DC, Brush);
  30.       Rectangle(DC, 0, 0, Rect.right, Rect.bottom); //Pintamos un rectángulo
  31.       DeleteObject(Brush);              //Destruimos la brocha
  32.       EndPaint(Wnd, ps);
  33.     end;
  34.     WM_DESTROY:
  35.       PostQuitMessage(0);                //Destruimos la ventana
  36.     else
  37.       // Función por defecto de tratamiento de mensajes.
  38.       Result:= DefWindowProc(Wnd, uMsg, wParam, lParam);
  39.   end;
  40. end;
  41.  
  42. function WinMain(hInstance, hPrevInstance: THISTANCE; lpCmdLine: LPSTR; nCmdShow: integer): cardinal; stdcall;
  43. var
  44.   Wnd: HWND;
  45.   WScreen, HScreen: integer;
  46.   WinClass: WNDCLASS;
  47.   Msg: TMsg;
  48. begin
  49.   WScreen:= GetSystemMetrics(SM_CXSCREEN);
  50.   HScreen:= GetSystemMetrics(SM_CYSCREEN);
  51.   ZeroMemory(@WinClass, sizeof(WinClass));
  52.   WinClass.lpfnWndProc:= @WindowProc;
  53.   WinClass.lpszClassName:= 'WindowsScreenSaverClass';
  54.   // Registramos nuestra clase de ventana
  55.   RegisterClass(WinClass);
  56.   //Creamos la ventana
  57.   Wnd:= CreateWindowEx(0, WinClass.lpszClassName, 'SCR', WS_VISIBLE + WS_OVERLAPPEDWINDOW,
  58.                         0, 0, WScreen, HScreen, HWND_DESKTOP, 0, 0, nil);
  59.   // El bucle de mensajes
  60.   while(GetMessage(Msg, 0, 0, 0)) do
  61.   begin
  62.     TranslateMessage(Msg);
  63.     DispatchMessage(Msg);
  64.   end;
  65.  
  66.   Result:= 0;
  67. end;
  68.  
  69. end.

Para no agobiar mas esta parte paramos en este punto. En la siguiente añadiremos mas funcionalidades a nuestro embrión de salvapantallas.



Saludos.


Edito para resubir el archivo de código

Archivos adjuntos


  • 0

#2 Wilson

Wilson

    Advanced Member

  • Moderadores
  • PipPipPip
  • 2.137 mensajes

Escrito 30 mayo 2013 - 06:18

Gracias maestro, como siempre, código de gran calidad.  (y)

Un abrazo.
  • 0

#3 escafandra

escafandra

    Advanced Member

  • Administrador
  • 4.107 mensajes
  • LocationMadrid - España

Escrito 30 mayo 2013 - 06:49

Parte 2

En esta entrega seguimos trabajando sobre el proyecto anterior. Añadimos lo siguiente:

- Quitamos el borde y botones de la ventana
- La pintamos de negro
- Añadimos respuesta a los mensajes de teclado y ratón.
- Añadimos respuesta retardada al movimiento del ratón.
- Escondemos el cursor del sistema.
- Implementamos el modo preview
- Forzar a nuestra ventana a permanecer en primer plano (no necesario).

Un aviso con este código, dado que esconde el cursor y fuerza la ventana el primer plano podemos llevarlos la desagradable sorpresa de perder el control si tratamos de debuguear el programa, por lo tanto estas opciones deben instalarse en la versión final ya depurada de esta fase. En este ejercicio podemos experimentar con el primer plano si no lo ejecutamos desde la configuración de pantalla->salvapantallas sino como una aplicación corriente.

La mecánica de pintar de negro la pantalla ya la conocemos. La respuesta a los mensajes de teclado y ratón será terminar el programa inmediatamente pero de forma controlada, como todo buen salvapantallas que se precie.

La ventana estará en primer plano si usamos el estilo WS_EX_TOPMOST al crearla, pero en las pruebas no conviene dejar activa esta opción.

Nuestra función de tratamiento de mensajes, hasta ahora el corazón del programa, queda así:


delphi
  1. function WindowProc(Wnd: HWND; uMsg: Cardinal; wParam, lParam: Integer): Integer; stdcall;
  2. const
  3. {$J+}
  4.   i: integer = 0;
  5. var
  6.   ps: PAINTSTRUCT;
  7.   DC: HDC;
  8.   Brush: HBRUSH;
  9.   Rect: TRect;
  10. begin
  11.   Result:= 0;
  12.   case uMsg of
  13.     WM_PAINT:
  14.     begin
  15.       DC:= BeginPaint(Wnd, ps);        //Comenzamos
  16.       GetWindowRect(Wnd, Rect);
  17.       Brush:= CreateSolidBrush(0);    //brocha negra
  18.       SelectObject(DC, Brush);
  19.       Rectangle(DC, 0, 0, Rect.right, Rect.bottom); //Pintamos un rectángulo
  20.       DeleteObject(Brush);            //Destruimos la brocha
  21.       EndPaint(Wnd, ps);
  22.     end;
  23.     WM_KEYDOWN,
  24.     WM_LBUTTONDOWN,
  25.     WM_MBUTTONDOWN,
  26.     WM_RBUTTONDOWN,
  27.     WM_MOUSEWHEEL:
  28.       if ScrMode = Full then PostQuitMessage(0); //Destruimos la ventana
  29.     WM_MOUSEMOVE:
  30.       if (i>15) and (ScrMode = Full) then PostQuitMessage(0) //Para dilatar un poco el movimiento del mouse
  31.       else inc(i);
  32.     WM_DESTROY,
  33.     WM_SYSCOMMAND:
  34.     begin
  35.       PostQuitMessage(0);
  36.     end
  37.     else
  38.     // Función por defecto de tratamiento de mensajes.
  39.     Result:= DefWindowProc(Wnd, uMsg, wParam, lParam);
  40.   end;
  41. {$J-}
  42. end;

La directiva {$J+}  es para conseguir modificar las constantes consiguiendo variables static, {$J-} anula lo anterior,

En el modo preview  la aplicación no terminará con teclado y ratón pero si con un WM_DESTROY y WM_ SYSCOMMAND. El truco para que se vea el preview en la ventanita del visor del salvapantallas está en darle estilo WS_CHILD y a su parent asignarle la ventanita en cuestión. Cuanto el visor del salvapantallas hace un preview arranca el salvapantallas con los parámetros /p y el valor numérico del Handle de la ventanita: /p handle. Sabiendo esto prepararemos el tamaño y parent de nuestro salvapantallas para ajustarlo al visor.

Nuestro prototipo ya es un salvapantallas pero de momento no hace nada mas que poner en negro la pantalla y no es configurable, aunque si hace un preview. Ya podemos dar la extensión “scr” en Projects Opcions ->Application->Target File Extensión = scr. Ahora ya podemos instalarlo y probarlo.

Esta será la función WinMain:


delphi
  1. function WinMain(hInstance, hPrevInstance: THISTANCE; lpCmdLine: LPSTR; nCmdShow: integer): cardinal; stdcall;
  2. var
  3.   WScreen, HScreen: integer;
  4.   WinClass: WNDCLASS;
  5.   Msg: TMsg;
  6.   ParentWnd: HWND;
  7.   PreviewRect: TRect;
  8.   Style: DWORD;
  9. begin
  10. //  ScrMode:= None;
  11.   ScrMode:= Full;
  12.   Style:= 0;
  13.   ParentWnd:= HWND_DESKTOP;
  14.  
  15.   if lpCmdLine <> nil then
  16.   case (lpCmdLine+1)^ of
  17.     's', 'S': ScrMode:= Full;
  18.     'p', 'P': ScrMode:= Preview;
  19.   end;
  20.  
  21.   if (ScrMode = Full) or (ScrMode = None) then
  22.   begin
  23.     // Indicamos a windows que comienza el salvapantallas...
  24.     SystemParametersInfo(SPI_SCREENSAVERRUNNING, 1, nil, 0);
  25.     ShowCursor(false);
  26.  
  27.     WScreen:= GetSystemMetrics(SM_CXSCREEN);
  28.     HScreen:= GetSystemMetrics(SM_CYSCREEN);
  29.     Style:= WS_VISIBLE + WS_POPUP;
  30.   end
  31.   else if  ScrMode = Preview then
  32.   begin
  33.     ParentWnd:= StrToIntA(GetParam(lpCmdLine, 1));
  34.     GetWindowRect(ParentWnd, PreviewRect);
  35.     WScreen:= PreviewRect.right-PreviewRect.left;
  36.     HScreen:= PreviewRect.bottom-PreviewRect.top;
  37.     Style:= WS_VISIBLE + WS_CHILD;
  38.   end;
  39.  
  40.   ZeroMemory(@WinClass, sizeof(WinClass));
  41.   WinClass.lpfnWndProc:= @WindowProc;
  42.   WinClass.lpszClassName:= 'WindowsScreenSaverClass';
  43.   // Registramos nuestra clase de ventana
  44.   RegisterClass(WinClass);
  45.   //Creamos la ventana en primer plano
  46.   //Wnd:= CreateWindowEx(WS_EX_TOPMOST, WinClass.lpszClassName, 'SCR', Style,
  47.   Wnd:= CreateWindowEx(0, WinClass.lpszClassName, '', Style,
  48.                         0, 0, WScreen, HScreen, ParentWnd, 0, 0, nil);
  49.  
  50.   // El bucle de mensajes
  51.   while(GetMessage(Msg, 0, 0, 0)) do
  52.   begin
  53.     TranslateMessage(Msg);
  54.     DispatchMessage(Msg);
  55.   end;
  56.  
  57.   // Indicamos a windows que terminó el salvapantallas...
  58.   if (ScrMode = Full) or (ScrMode = None) then
  59.     SystemParametersInfo(SPI_SCREENSAVERRUNNING, 0, nil, 0);
  60.   Result:= 0;
  61. end;

Señalar que debemos indicar a Windows cuando arranca y cuando termina el salvapantallas con SystemParametersInfo.

El siguiente paso será dotarle de la funcionalidad para representar las fotografías de la carpeta donde se encuentre y en varios formatos de archivo pero eso lo vamos a implementar en la siguiente entrega.

Repito el peligro de hacer un debug desde delphi con esta versión forzada de primer plano y sin cursor de ratón. El ide de delphi no será controlable con nuestra ventana en primer plano y su código parado por delphi. Para experimentar cambiar WS_EX_TOPMOST por 0.



Saludos.

Edito para resubir el archivo adjunto de código


  • 0

#4 poliburro

poliburro

    Advanced Member

  • Administrador
  • 4.945 mensajes
  • LocationMéxico

Escrito 30 mayo 2013 - 07:37

Increible¡¡¡¡¡¡¡¡¡¡. Amigo escafandra muchas gracias por compartirnos tu conocimiento.  Esto va directo al face. :D
  • 0

#5 Wilson

Wilson

    Advanced Member

  • Moderadores
  • PipPipPip
  • 2.137 mensajes

Escrito 30 mayo 2013 - 09:27

Maestro Escafandra, una línea de código vale más que mil palabras..., infinitas gracias por compartirnos una parte de tu enorme conocimiento.  (y)

[off-topic]

Esto va directo al face. :D

Poli, no crees que estamos ya muy viejitos para andar con el face?  :D :D :D
[/off-topic]
  • 0

#6 poliburro

poliburro

    Advanced Member

  • Administrador
  • 4.945 mensajes
  • LocationMéxico

Escrito 30 mayo 2013 - 09:42


[off-topic]
Poli, no crees que estamos ya muy viejitos para andar con el face?  :D :D :D
[/off-topic]


[off-topic]
    jajajaja amigo Wilson la piel se arruga, pero  por dentro aún somos unos chamacones, :D
[/off-topic]
  • 0

#7 egostar

egostar

    missing my father, I love my mother.

  • Administrador
  • 14.448 mensajes
  • LocationMéxico

Escrito 30 mayo 2013 - 10:49

[off-topic]
Poli, no crees que estamos ya muy viejitos para andar con el face?  :D :D :D
[/off-topic]


[off-topic]
Me consta que al buen compañero poliburro, se le refrescó la "sangre de puma" y recobró las ganas de escribir, ahora es todo un jovenzuelo imberbe queriendose comer el mundo a puños :D :D :D
[/off-topic]

Que buen código amigo escafandra, como siempre, nos regalas un pedazo de inteligencia :) (y)

Saludos
  • 0

#8 escafandra

escafandra

    Advanced Member

  • Administrador
  • 4.107 mensajes
  • LocationMadrid - España

Escrito 30 mayo 2013 - 12:18

Parte 3

Si hemos entendido las dos primeras partes, estamos en disposición de dar un salto y complicar el código de nuestro embrión. Vamos a volcar imágenes en nuestra ventana desde archivos. Para ello nos vamos a apoyar en GDI plus, tema sobre el que ya se ha sido publicado en el foro.

El código fundamental está centrado en el tratamiento del mensaje WM_PAINT y  funciones de apoyo.


Dibujando un Bitmap en una ventana:

Para dibujar un Bitmap en una ventana procederemos de la siguiente forma:



delphi
  1. var
  2.   ps: PAINTSTRUCT;
  3.   DC, dcMem: HDC;
  4.   Brush: HBRUSH;
  5.   bmOld: HBITMAP;
  6.   Rect: TRect;
  7. begin
  8.   case uMsg of
  9.     WM_PAINT:
  10.     begin
  11.       DC:= BeginPaint(Wnd, ps);        //Comenzamos
  12.       GetWindowRect(Wnd, Rect);
  13.       Brush:= CreateSolidBrush(0);    //brocha negra
  14.       SelectObject(DC, Brush);
  15.       dcMem:= CreateCompatibleDC(DC);
  16.       bmOld:= SelectObject(dcMem, Image);
  17.  
  18.       // Rellenamos la pantalla de negro
  19.       Rectangle(DC, 0, 0, Rect.right, Rect.bottom);
  20.      
  21.       // Pintamos el bitmap
  22.       BitBlt(DC, 0, 0, Rect.right, Rect.bottom, dcMem, 0, 0, SRCCOPY);
  23.  
  24.       // Destruimos lo que ya no hace falta
  25.       SelectObject(dcMem, bmOld);
  26.       DeleteDC(dcMem);
  27.       DeleteObject(Brush);           
  28.       EndPaint(Wnd, ps);
  29.     end;
  30. …………….

Pintamos primero de negro para no dejar feos huecos sin pintar que contendrán las imágenes que estén detrás de nuestra ventana. Este código vuelca el HBITMAP Image en el DC de nuestra ventana salvapantallas. Ahora nos podemos plantear que si el tamaño del bitmap es menor que la pantalla, parte quedará en negro, pero si es mayor, parte se perderá. Además la imagen no estará centrada en la pantalla. Este código podemos modificarlo para centrar nuestro bitmap, pero lo ideal es que nuestra imagen trate de aprovechar al máximo la pantalla. Para conseguir esto echamos mano de GDI plus y de código que ya publiqué en el foro para adaptarlo a esta necesidad.


Dibujar un Bitmap centrado y aprovechando toda la pantalla:

Lo que vamos a hacer para optimizar el rendimiento es pasar, al tratamiento WM_PAINT, un HBITMAP ya preparado en Image que va a ser tan grande como la pantalla y contendrá centrada la imagen que adaptará su tamaño para aprovechar al máximo las dimensiones respetando sus proporciones.
 

delphi
  1. function LoadBitmapW(FileName: PWCHAR): HBITMAP;
  2. var
  3.   bm: BITMAP;
  4.   Rect: TRECT;
  5.   Bitmap, bmOld: HBITMAP;
  6.   DC, dcMem: HDC;
  7.   Brush: HBRUSH;
  8.   H, W, Ws, Hs: integer;
  9. begin
  10.   Result:= 0;
  11.   if not IsGraphFileName(FileName) then exit;
  12.   Bitmap:= CreateHBITMAPFromFileW(FileName);
  13.   if Bitmap = 0 then exit;
  14.  
  15.   GetWindowRect(Wnd, Rect);
  16.   GetObject(Bitmap, sizeof(bm), @bm);
  17.   GetWindowRect(Wnd, Rect);
  18.  
  19.   // Calculando tamaño de la imagen para ajustarse a la pantalla
  20.   Ws:= Rect.right - Rect.left;
  21.   Hs:= Rect.bottom - Rect.top;
  22.   H:= bm.bmHeight;
  23.   W:= bm.bmWidth;
  24.  
  25.   // Comparando proporciones de pantalla e imagen para ajustar el tamaño
  26.   if(W/H > Ws/Hs) then
  27.   begin
  28.     H:= MulDiv(Ws, H, W);
  29.     W:= Ws;
  30.   end
  31.   else
  32.   begin
  33.     W:= MulDiv(Hs, W, H);
  34.     H:= Hs;
  35.   end;
  36.  
  37.   DC:= GetDC(0);
  38.   dcMem:= CreateCompatibleDC(0);
  39.   Result:= CreateCompatibleBitmap(DC, Ws, Hs);
  40.   bmOld:= SelectObject(dcMem, Result);
  41.   GetObject(Result, sizeof(bm), @bm);
  42.   Brush:= CreateSolidBrush(0);
  43.   SelectObject(dcMem, Brush);
  44.   Rectangle(dcMem, 0, 0, Rect.right, Rect.bottom);
  45.  
  46.   // Dibujando la imagen con el nuevo tamaño, centrada y respetando proporciones iniciales
  47.   DrawImageRect(dcMem, Bitmap, (Ws-W) div 2, (Hs-H) div 2, W, H);
  48.  
  49.   DeleteObject(Bitmap);
  50.   SelectObject(dcMem, bmOld);
  51.   DeleteDC(dcMem);
  52.   DeleteObject(Brush);
  53.   ReleaseDC(0, DC);
  54. end;

Veis que el código anterior comprueba si la extensión es de un formato gráfico conocido por GDI plus. Esto realmente no es necesario, pero lo he implementado para dar mas velocidad en el caso de imágenes existentes en carpetas con infinidad de archivos no gráficos (véase \Windows\system32\*.*)

CreateHBITMAPFromFileW y DrawImageRect son funciones que usan GDI plus y ya publicadas en el foro, o alguna variante de las mismas.

Por si hubiese problemas con los archivos adjuntos las voy a escribir:
 

delphi
  1. // Crea un HBITMAP desde un fichero usando GDI+
  2. function CreateHBITMAPFromFileW(FileName: PWCHAR): HBITMAP;
  3. var
  4.   Bitmap: HBITMAP;
  5.   GBitmap: THANDLE;
  6. begin
  7.   Bitmap:= 0;
  8.   GBitmap:= 0;
  9.   GdipCreateBitmapFromFile(FileName, GBitmap);
  10.   GdipCreateHBITMAPFromBitmap(GBitmap, Bitmap, 0);
  11.   GdipDisposeImage(GBitmap);
  12.   Result:= Bitmap;
  13. end;
  14.  
  15. // Dibuja un Bitmap en un hDC ajustando su tamaño para que quepa entero
  16. // en las coordenadas dadas
  17. procedure DrawImageRect(DC: HDC; Bitmap: HBITMAP; X, Y, W, H: integer);
  18. var
  19.   Graphics: Pointer;
  20.   GBitmap:  THANDLE;
  21. begin
  22.   GdipCreateBitmapFromHBITMAP(Bitmap, 0, GBitmap);
  23.   GdipCreateFromHDC(DC, Graphics);
  24.   GdipDrawImageRect(Graphics, GBitmap, X, Y, W, H);
  25.   GdipDisposeImage(GBitmap);
  26.   GdipDeleteGraphics(Graphics);
  27. end;


Simplificando WindowProc:

Con la filosofía expuesta podemos simplificar algo mas el tratamiento de WM_PAINT.



delphi
  1.     WM_PAINT:
  2.     begin
  3.       DC:= BeginPaint(Wnd, ps);       
  4.       GetWindowRect(Wnd, Rect);
  5.       dcMem:= CreateCompatibleDC(DC);
  6.       bmOld:= SelectObject(dcMem, Image);
  7.  
  8.       BitBlt(DC, 0, 0, Rect.right, Rect.bottom, dcMem, 0, 0, SRCCOPY);
  9.  
  10.       SelectObject(dcMem, bmOld);
  11.       DeleteDC(dcMem);
  12.       EndPaint(Wnd, ps);
  13.     end;

Con esto tenemos resuelto el tema de la carga de imágenes pero nos falta como renovarlas.


Renovando las imágenes mostradas:

Para renovar las imágenes mostradas usaremos un timer que se encargará de recorrer los archivos de una carpeta y encontrar aquellos formatos gráficos que nuestro salvapantallas pueda representar con GDI plus (BMP, GIF, JPEG, PNG, TIFF, WMF, EMF, ICON), para proceder a ello.

La API SetTimer pone en marcha un timer y se para con KillTimer

La siguiente es la función que usaremos para el timer que renovará las imágenes de nuestro salvapantallas:
 

delphi
  1. // Función de timer para cargar y mostrar una nueva imagen
  2. procedure NewImage(Wnd: HWND; uMsg: UINT; idEvent: UINT; dwTime: DWORD); stdcall;
  3. const
  4. {$J+}
  5.   hFind: THANDLE = INVALID_HANDLE_VALUE;
  6.   fd: WIN32_FIND_DATAW = ();
  7.   Next: boolean = false;
  8. var
  9.   Count: integer;
  10. begin
  11.   KillTimer(Wnd, New);
  12.   ShowMouseCursor(false);
  13.  
  14.   if Image<>0 then DeleteObject(Image);
  15.   Image:= 0;
  16.   Count:= 0;
  17.   while (Image = 0) and (Count = 0) do
  18.   begin
  19.     if not Next then
  20.     begin
  21.       hFind:= FindFirstFileW('*.*', fd);
  22.       inc(Count);
  23.     end;
  24.     repeat
  25.       if fd.dwFileAttributes = FILE_ATTRIBUTE_ARCHIVE then
  26.         Image:= LoadBitmapW(fd.cFileName);
  27.       Next:= FindNextFileW(hFind, fd);
  28.     until not Next or (Image <> 0);
  29.     if Image <> 0 then ReDrawWindow(Wnd);
  30.     if not Next then FindClose(hFind);
  31.   end;
  32.   SetTimer(Wnd, New, NewTime, @NewImage);
  33. {$J-}
  34. end;

Esta función para el timer en su inicio para restaurarlo al final, con el fin de comenzar una cuenta nueva cuando termine. Su cometido es recorrer todos los archivos del directorio de trabajo y tratar de cargarlos como imagen, si lo consigue guarda el HBITMAP en la variable Image y obliga a repintar la ventana. Después deja todo preparado para la siguiente entrada de timer para lo que debe tener “memoria” de donde se quedó la última vez eso lo conseguimos con la directiva{$J+} que convierte las constantes en variables estáticas de delphi, característica que muchos pensarán que no tenía. Al terminar la carpeta se reinicia de nuevo la búsqueda.


Rescribiendo la función WindowProc:

Finalmente nuestra función WinMain debe ser adaptada para inicializar el timer al inicio del programa y para inicializar GDI plus:
 

delphi
  1. function WinMain(hInstance, hPrevInstance: THISTANCE; lpCmdLine: LPSTR; nCmdShow: integer): cardinal; stdcall;
  2. var
  3.   WScreen, HScreen: integer;
  4.   WinClass: WNDCLASS;
  5.   Msg: TMsg;
  6.   ParentWnd: HWND;
  7.   PreviewRect: TRect;
  8.   Style: DWORD;
  9.   gdiplusToken: DWORD;
  10.   GdiPlusStartupInput: array[0..2] of int64;
  11. begin
  12.  
  13.   GetModuleFileName(0, Buffer, sizeof(Buffer));
  14.   SetCurrentDirectory(GetAppPath(Buffer, sizeof(Buffer)));
  15.  
  16.   // Inicializamos GDI+.
  17.   GdiPlusStartupInput[0]:= 1; GdiPlusStartupInput[1]:= 0;
  18.   GdiplusStartup(gdiplusToken, @GdiPlusStartupInput, nil);
  19.  
  20.   ScrMode:= Full;
  21.   Style:= 0;
  22.   ParentWnd:= HWND_DESKTOP;
  23.  
  24.   if lpCmdLine <> nil then
  25.   case (lpCmdLine+1)^ of
  26.     's', 'S': ScrMode:= Full;
  27.     'p', 'P': ScrMode:= Preview;
  28.   end;
  29.  
  30.   if (ScrMode = Full) or (ScrMode = None) then
  31.   begin
  32.     // Indicamos a windows que comienza el salvapantallas...
  33.     SystemParametersInfo(SPI_SCREENSAVERRUNNING, 1, nil, 0);
  34.     ShowCursor(false);
  35.  
  36.     WScreen:= GetSystemMetrics(SM_CXSCREEN);
  37.     HScreen:= GetSystemMetrics(SM_CYSCREEN);
  38.     Style:= WS_VISIBLE + WS_POPUP;
  39.   end
  40.   else if  ScrMode = Preview then
  41.   begin
  42.     NewTime:= 3000;
  43.     ParentWnd:= StrToIntA(GetParam(lpCmdLine, 1));
  44.     GetWindowRect(ParentWnd, PreviewRect);
  45.     WScreen:= PreviewRect.right-PreviewRect.left;
  46.     HScreen:= PreviewRect.bottom-PreviewRect.top;
  47.     Style:= WS_VISIBLE + WS_CHILD;
  48.   end;
  49.  
  50.   ZeroMemory(@WinClass, sizeof(WinClass));
  51.   WinClass.lpfnWndProc:= @WindowProc;
  52.   WinClass.lpszClassName:= 'WindowsScreenSaverClass';
  53.   // Registramos nuestra clase de ventana
  54.   RegisterClass(WinClass);
  55.   //Creamos la ventana en primer plano
  56.   //Wnd:= CreateWindowEx(WS_EX_TOPMOST, WinClass.lpszClassName, '', Style,
  57.   Wnd:= CreateWindowEx(0, WinClass.lpszClassName, '', Style,
  58.                         0, 0, WScreen, HScreen, ParentWnd, 0, 0, nil);
  59.   // Evento Timer.
  60.   SetTimer(Wnd, New, NewTime, @NewImage);
  61.   // Fuerzo cargar la primera imagen
  62.   NewImage(Wnd, 0, 0, 0);
  63.  
  64.   // El bucle de mensajes
  65.   while(GetMessage(Msg, 0, 0, 0)) do
  66.   begin
  67.     TranslateMessage(Msg);
  68.     DispatchMessage(Msg);
  69.   end;
  70.  
  71.   // Cerramos GDI+
  72.   GdiplusShutdown(gdiplusToken);
  73.   ShowCursor(true);
  74.   Result:= 0;
  75. end;


Principales parámetros de entrada de un salvapantallas:

He de recalcar que un salvapantallas debe dar respuesta a los siguientes parámetros de entrada al programa:

/s La respuesta será poner en marcha el salvapantallas y es así como le arranca el sistema cuando lo pone en marcha.
/p Es el parámetro que define el modo preview. El salvamantallas se mostrará en una ventanita del sistema mostrando su funcionalidad, para ello el sistema tambien pasa el valor del handle de la ventana del visor de esta forma: /p handle.
/c Es el modo configuración. El salvapantallas mostrará una ventana de dialogo para configurar sus características.

Llegados a este punto hemos conseguido nuestro objetivo. Tenemos un salvapantallas que está escrito para Windows usando sólo la API y en delphi. No hemos escatimado esfuerzos incluso para el tratamiento de cadenas a bajo nivel. Nuestro salvapantallas muestra las Imágenes contenidas en una carpeta, sea cual sea su formato y dispone de modo preview.

El ejercicio de ejemplo puede irse complicando mucho mas añadiendo un dialogo de configuración, transiciones entre imágenes y lo que la imaginación nos dicte, pero posiblemente ya sea suficiente para este minitutorial.

Espero no haber aburrido y que sea de utilidad para entender mas a fondo como trabaja Windows.

Añado todo el código que he ido escribiendo para este tutorial.



Saludos.

Edito para arreglar etiquetas de código

Archivos adjuntos


  • 0

#9 ELKurgan

ELKurgan

    Advanced Member

  • Miembro Platino
  • PipPipPip
  • 566 mensajes
  • LocationEspaña

Escrito 30 mayo 2013 - 11:17

Excelente tutorial, como siempre

Muchas gracias, maestro

(y) (y) (y)
  • 0

#10 escafandra

escafandra

    Advanced Member

  • Administrador
  • 4.107 mensajes
  • LocationMadrid - España

Escrito 06 junio 2013 - 03:51

Parte 4: Suavizando el cambio de imágenes

La parte tres del tutorial sugería el uso de transiciones entre las imágenes representadas y acordándome de un tutorial que publiqué denominado Transparencias no he podido contenerme y he decidido ampliar el tema para aplicar un efecto de fundido entre imágenes.


Transición de fundido entre imágenes:

El fundido lo realizaremos pintando sobre la antigua imagen otra nueva con grado creciente de opacidad hasta verse sólo la nueva imagen. El efecto de transparencia se consigue con la API AlphaBlend. Y para que el efecto sea visualmente progresivo vamos a usar un thread. Me parece interesante incluir este concepto en este tutorial aunque ya escribí otro específico del tema: Threads con Windows API. Un tutorial sencillo.

Comenzamos adaptando la API AlphaBlend al uso que le daremos. Como hemos diseñado el salvapantallas guardando un HBITMAP que usará el mensaje WM_PAINT, vamos a adaptar AlphBlend para usar bitmaps en lugar de HDC. Para ello creamos un CompatibleDC al que asociamos un bitmap que usaremos como parámetro. Val será el valos de opacidad entre 0 y 255. Debemos desasociar nuestro DC del bitmap antes de destruirlo.
 


delphi
  1. // Dibuja un Bitmap sobre otro de forma semitransparente
  2. // Val: valor de opacidad
  3. procedure DrawSemiTransparent(Tbmp, Sbmp: HBITMAP; Val: integer);
  4. var
  5. bm: BITMAP;
  6. bf: BLENDFUNCTION;
  7. SdcMem, TdcMem: HDC;
  8. SbmOld, TbmOld: HBITMAP;
  9. begin
  10. bf.BlendOp:= AC_SRC_OVER;
  11. bf.BlendFlags:= 0;
  12. bf.SourceConstantAlpha:= Val;
  13. bf.AlphaFormat:= 0;
  14. GetObject(Sbmp, sizeof(bm), @bm);
  15. SdcMem:= CreateCompatibleDC(0);
  16. TdcMem:= CreateCompatibleDC(0);
  17. SbmOld:= SelectObject(SdcMem, Sbmp);
  18. TbmOld:= SelectObject(TdcMem, Tbmp);
  19. Windows.AlphaBlend(TdcMem, 0, 0, bm.bmWidth, bm.bmHeight, SdcMem, 0, 0, bm.bmWidth, bm.bmHeight, bf);
  20. SelectObject(SdcMem, SbmOld);
  21. DeleteObject(SdcMem);
  22. SelectObject(TdcMem, TbmOld);
  23. DeleteObject(TdcMem);
  24. end;

Usando threads

Ahora vamos a crear el efecto usando varias veces la anterior función hasta la opacidad total. Para el efecto usamos un thread.

Recordemos que un thread es un hilo de código que corre paralelo y simultaneo con el hilo principal de la aplicación. Se pone en marcha con la API CreateThread (recordar el tutorial antes citado) Entre sus parámetros se encuentran dos muy importantes, el primero la función del thread y el segundo un puntero que se pasa a la función del thread y que suele ser un puntero a una estructura de datos (record), es decir, es el parámetro de la función del thread. Nosotros no necesitamos parámetros pues los datos que usará nuestro thread son variables globales.

La función del thread es la siguiente:


delphi
  1. function Fundido(): DWORD; stdcall;
  2. var
  3. i: WORD;
  4. begin
  5. i:= 0;
  6. repeat
  7. i:= i+4;
  8. DrawSemiTransparent(Image, NewBitmap, i-1);
  9. Sleep(10);
  10. ReDrawWindow(Wnd);
  11. until i > 255;
  12. DeleteObject(NewBitmap); NewBitmap:= 0;
  13. Result:= 0;
  14. end;

Como veis es un bucle que va dando valores de opacidad a NewBitmap sobre Image y redibujando la ventana en cada iteración. Fácil, pero podemos preguntar para que demonios necesitamos un thread. La explicación es sencilla. Si queremos que nuestro salvapantallas reaccione con rapidez a un movimiento de ratón o pulsación de una tecla no podemos permitir que la aplicación se enganche en un bucle. Un thread dará un aspecto mas profesional al rendimiento deseado. Así que os propongo esta solución sencilla:
 


delphi
  1. procedure Transit();
  2. function Fundido(): DWORD; stdcall;
  3. var
  4. i: WORD;
  5. begin
  6. i:= 0;
  7. repeat
  8. i:= i+4;
  9. DrawSemiTransparent(Image, NewBitmap, i-1);
  10. Sleep(10);
  11. ReDrawWindow(Wnd);
  12. until i > 255;
  13. DeleteObject(NewBitmap); NewBitmap:= 0;
  14. Result:= 0;
  15. end;
  16. var
  17. DC: HDC;
  18. begin;
  19. if NewBitmap = 0 then
  20. begin
  21. // Si no hay Bitmap creo uno vacio
  22. DC:= GetDC(0);
  23. NewBitmap:= CreateCompatibleBitmap(DC, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
  24. ReleaseDC(0, DC);
  25. end;
  26. CloseHandle(CreateThread(nil, 0, @Fundido, nil, 0, PDWORD(0)^));
  27. end;

La parte final asegura que NewBitmap va a ser válido y haremos un fundido aunque sea en negro antes de poner en marcha el thread.

Podemos acelerar o retrasar el efecto jugando son los valores del bucle y la espera de la API sleep


Adaptando nuestro salvapantallas:

Ahora sólo cabe realizar alguna modificación en el código del salvapantallas para introducir estas dos nuevas funciones:

Añadimos otra variable global NewBitmap que es un HBITMAP y tiene la nueva imagen a para fundir con la previa y modificamos ligeramente el procedimiento NewImage, llamada por un timer para actualizar la imagen.
 


delphi
  1. procedure NewImage(Wnd: HWND; uMsg: UINT; idEvent: UINT; dwTime: DWORD); stdcall;
  2. const
  3. {$J+}
  4. hFind: THANDLE = INVALID_HANDLE_VALUE;
  5. fd: WIN32_FIND_DATAW = ();
  6. Next: boolean = false;
  7. var
  8. Count: integer;
  9. begin
  10. KillTimer(Wnd, New);
  11.  
  12. if NewBitmap<>0 then DeleteObject(NewBitmap);
  13. NewBitmap:= 0;
  14. Count:= 0;
  15. while (NewBitmap = 0) and (Count = 0) do
  16. begin
  17. if not Next then
  18. begin
  19. hFind:= FindFirstFileW('*.*', fd);
  20. inc(Count);
  21. end;
  22. repeat
  23. if fd.dwFileAttributes = FILE_ATTRIBUTE_ARCHIVE then
  24. NewBitmap:= LoadBitmapW(fd.cFileName);
  25. Next:= FindNextFileW(hFind, fd);
  26. until not Next or (NewBitmap <> 0);
  27. if NewBitmap<>0 then Transit;
  28. if not Next then FindClose(hFind);
  29. end;
  30. Transit;
  31. SetTimer(Wnd, New, NewTime, @NewImage);
  32. {$J-}
  33. end;

Ya podemos probar nuestro efecto.


Capturando el contenido de una ventana:

Las transiciones son ahora suaves pero, al iniciar el salvapantallas, la primera imagen se muestra bruscamente. La solución es capturar la pantalla antes de iniciar el volcado de esa primera imagen, así el efecto es completo y logramos rizar el rizo. En realidad no vamos a capturar la pantalla sino el contenido de nuestra ventana salvapantallas tras crearla y antes de comenzar el bucle de mensajes pues entonces será transparente y contiene la pantalla en si misma. ¿Y entonces porque no capturar la pantalla?, pues porque recordad que nuestro salvapantallas hace preview y quedaría muy feo ver en la ventanita preview como aparece un fragmento de pantalla completa para luego fundirse con la primera imagen. Con nuestra técnica fundiría sobre el contenido previo de la ventanita.

Escribamos el código para capturar el contenido de una ventana. Si os fijáis capturo el área cliente pero podría capturarse completa. La técnica ya la conocéis con el uso de la API BitBlt, que ya hemos usado otras veces, para copiar un HDC (Canvas), en este caso el contenido gráfico de la ventana, en otro que está asociado a un HBITMAP que será el resultado de nuestra función. Como esta función crea un HBITMAP, deberemos ser responsables de su destrucción posterior.
 


delphi
  1. function CreateCaptureWindow(Wnd: HWND): HBITMAP;
  2. var
  3. Rect: TRECT;
  4. ScreenDC, DC: HDC;
  5. oldbmp: HBITMAP;
  6. begin
  7. GetClientRect(Wnd, Rect);
  8. ScreenDC:= GetDC(Wnd);
  9. DC:= CreateCompatibleDC(0);
  10. Result:= CreateCompatibleBitmap(ScreenDC, Rect.Right, Rect.Bottom);
  11. oldbmp:= SelectObject(DC, Result);
  12. BitBlt(DC, 0, 0, Rect.Right, Rect.Bottom, ScreenDC, 0, 0, SRCCOPY);// + $40000000);
  13. ReleaseDC(Wnd, ScreenDC);
  14. SelectObject(DC, oldbmp);
  15. DeleteObject(DC);
  16. end;

Modificando WinMain:

Hagamos los ajustes necesarios en WinMain:
 


delphi
  1. function WinMain(hInstance, hPrevInstance: THISTANCE; lpCmdLine: LPSTR; nCmdShow: integer): cardinal; stdcall;
  2. var
  3. WScreen, HScreen: integer;
  4. WinClass: WNDCLASS;
  5. Msg: TMsg;
  6. ParentWnd: HWND;
  7. PreviewRect: TRect;
  8. Style: DWORD;
  9. gdiplusToken: DWORD;
  10. GdiPlusStartupInput: array[0..2] of int64;
  11. DC: HDC;
  12. begin
  13.  
  14. // Establecemos el directorio de trabajo
  15. SetCurrentDirectory(GetAppPath(Buffer, sizeof(Buffer)));
  16.  
  17. // Inicializamos GDI+.
  18. GdiPlusStartupInput[0]:= 1; GdiPlusStartupInput[1]:= 0;
  19. GdiplusStartup(gdiplusToken, @GdiPlusStartupInput, nil);
  20.  
  21. ScrMode:= Full;
  22. Style:= 0;
  23. ParentWnd:= HWND_DESKTOP;
  24.  
  25. if lpCmdLine <> nil then
  26. case (lpCmdLine+1)^ of
  27. 's', 'S': ScrMode:= Full;
  28. 'p', 'P': ScrMode:= Preview;
  29. end;
  30.  
  31. if (ScrMode = Full) or (ScrMode = None) then
  32. begin
  33. // Indicamos a windows que comienza el salvapantallas...
  34. SystemParametersInfo(SPI_SCREENSAVERRUNNING, 1, nil, 0);
  35. ShowCursor(false);
  36.  
  37. WScreen:= GetSystemMetrics(SM_CXSCREEN);
  38. HScreen:= GetSystemMetrics(SM_CYSCREEN);
  39. Style:= WS_VISIBLE + WS_POPUP;
  40. end
  41. else if ScrMode = Preview then
  42. begin
  43. NewTime:= 3000;
  44. ParentWnd:= StrToIntA(GetParam(lpCmdLine, 1));
  45. GetWindowRect(ParentWnd, PreviewRect);
  46. WScreen:= PreviewRect.right-PreviewRect.left;
  47. HScreen:= PreviewRect.bottom-PreviewRect.top;
  48. Style:= WS_VISIBLE + WS_CHILD;
  49. end;
  50.  
  51. ZeroMemory(@WinClass, sizeof(WinClass));
  52. WinClass.lpfnWndProc:= @WindowProc;
  53. WinClass.lpszClassName:= 'WindowsScreenSaverClass';
  54. // Registramos nuestra clase de ventana
  55. RegisterClass(WinClass);
  56. //Creamos la ventana en primer plano
  57. Wnd:= CreateWindowEx(WS_EX_TOPMOST, WinClass.lpszClassName, '', Style,
  58. // Wnd:= CreateWindowEx(0, WinClass.lpszClassName, '', Style,
  59. 0, 0, WScreen, HScreen, ParentWnd, 0, 0, nil);
  60.  
  61. Image:= CreateCaptureWindow(Wnd);
  62.  
  63. // Fuerzo cargar la primera imagen
  64. NewImage(Wnd, 0, 0, 0);
  65.  
  66. // Evento Timer.
  67. SetTimer(Wnd, New, NewTime, @NewImage);
  68. // El bucle de mensajes
  69. while(GetMessage(Msg, 0, 0, 0)) do
  70. begin
  71. TranslateMessage(Msg);
  72. DispatchMessage(Msg);
  73. end;
  74.  
  75. if Image <> 0 then DeleteObject(Image);
  76. if NewBitmap <> 0 then DeleteObject(NewBitmap);
  77.  
  78. // Cerramos GDI+
  79. GdiplusShutdown(gdiplusToken);
  80. ShowCursor(true);
  81. Result:= 0;
  82. end;

Como veis, poco hemos cambiado. Iniciamos el HBITMAP Image con el contenido de la ventana principal antes del bucle de mensajes, con lo que en realidad contiene todo lo que esté detrás de ella ocupado por sus dimensiones (pantalla completa). Para terminar nos aseguramos que tanto Image como NewBitmap sean destruidos al finalizar el programa.
Notad que como ambos bitmaps se crean y destruyen cada vez que se renueva una imagen, en cada destrucción nos aseguramos previamente de que no es nulo y tras destruirlo le asignamos este valor. Así nos aseguramos de destruir algo válido y no previamente destruido.

Hemos tocado aspectos básicos de la programación bajo Windows con la API, ventanas y mensajes. También hemos visto aspectos gráficos con GDI y GDI plus, manejo de varios formatos gráficos, transparencias y capturas.

Espero que esta última 4ª parte haya sido de vuestro agrado.



Saludos.

Edito para resubir adjunto de código

Archivos adjuntos


  • 0




IP.Board spam blocked by CleanTalk.