1. 程式人生 > >Windows程序設計核心總結(打印機-2018.5.5)

Windows程序設計核心總結(打印機-2018.5.5)

Windows程序設計核心總結

本人大三學生,自學Windows程序設計有兩三個月了,我是看魚C工作室發布的Windows程序設計視頻入門的,這視頻集數雖然不是特別多,目前只有前面九章的視頻內容,但小甲魚老師講解書本內容十分詳細、入微,能讓我們學習到不少知識。我開發Win32的環境是VS2013。

一、打印機工作機理

在Windows中使用打印機時,實際上啟動了一系列模塊之間復雜的交互過程,包括GDI32模塊、打印機設備驅動程序模塊、Windows後臺打印處理程序和其他模塊。
應用程序想要開始使用打印機,首先調用CreateDC函數獲取打印機設備環境句柄,而該參數必須需要知道打印機設備名,所以還需要先待用EnumPrinters函數獲取打印機設備名。註意,當調用了CreateDC函數後,參數相應的打印機設備驅動程序被載入內存。應用程序再調用StartDoc函數開始新文檔,該函數由GDI模塊處理,GDI模塊調用剛剛被調入內存中的打印機設備驅動程序中的Control函數,告訴設備驅動程序做好打印準備。然後調用StartPage函數開始新的一頁,以EndPage函數結束這一頁,註意,在StartPage和EndPage函數之間調用GDI函數開始在頁面繪制,這時GDI模塊就會先將這些GDI繪制函數存儲到硬盤上的圖元文件上。好了,調用完EndPage函數結束了這一頁,那麽真正的打印工作開始了,打印機設備驅動程序就會將硬盤上的圖元文件轉化成適用於打印機的輸出,具體怎麽轉換我們不關心。接著,這些轉化後的打印機輸出會被GDI模塊存儲到另一個臨時文件中,到這為止,這一頁的所有工作都完成了,那就要進行下一頁的打印了,怎麽告訴後臺打印處理程序需要打印新的一頁?GDI模塊會采用進程間調用告訴後臺打印處理程序新的作業已就緒,應用程序應該處理下一個頁面了,循環反復...將所有頁面都打印完後,就可以調用EndDoc函數表示打印作業完成。

二、獲取打印機設備環境句柄

我們知道,要使用打印機,必須首先獲取打印機設備環境句柄,一般地,通過調用CreateDC函數獲取打印機設備環境句柄,但是要註意的問題是,該函數的參數2需要指定打印機設備的名稱。打印機設備的名稱怎麽來?我們都知道,一臺計算機可以同時連接多臺打印機,而不管連接多少臺打印機,默認打印機只有一臺,默認打印機就是用戶最近一次選用的打印機。所以,我們可以獲取默認打印機設備的名稱,通過調用EnumPrinters函數獲取默認打印機的名稱,再將該名稱作為CreateDC函數的參數。下面是完整的獲取打印機設備環境句柄的代碼例子:

HDC GetPrinterDC(void)
{
    DWORD            dwNeeded, dwReturned;
    HDC              hdc;
    PRINTER_INFO_4 * pinfo4;
    PRINTER_INFO_5 * pinfo5;
    if (GetVersion() & 0x80000000)         // Windows 98
    {
    //第一次調用該函數是為了得到所需的結構大小
        EnumPrinters(PRINTER_ENUM_DEFAULT, NULL, 5, NULL,
            0, &dwNeeded, &dwReturned);
        pinfo5 = (PRINTER_INFO_5 *)malloc(dwNeeded);
//第二次調用該函數才是真正填充該結構
        EnumPrinters(PRINTER_ENUM_DEFAULT, NULL, 5, (PBYTE)pinfo5,
            dwNeeded, &dwNeeded, &dwReturned);
//將獲取的結構裏的pPrinterName成員作為CreateDC函數的參數
        hdc = CreateDC(NULL, pinfo5->pPrinterName, NULL, NULL);
        free(pinfo5);
    }
    else                                    // Windows NT
    {
    //下面同理
        EnumPrinters(PRINTER_ENUM_LOCAL, NULL, 4, NULL,
            0, &dwNeeded, &dwReturned);
        pinfo4 = (PRINTER_INFO_4 *)malloc(dwNeeded);
        EnumPrinters(PRINTER_ENUM_LOCAL, NULL, 4, (PBYTE)pinfo4,
            dwNeeded, &dwNeeded, &dwReturned);
        hdc = CreateDC(NULL, pinfo4->pPrinterName, NULL, NULL);
        free(pinfo4);
    }
    //返回打印機設備環境句柄
    return hdc;
}

三、打印圖形和文字

我們在上面的代碼中辣麽辛苦獲取了打印機設備環境句柄,那麽我們該怎麽使用它呢?不急,我們先放放。我們先創建一個應用程序窗口,在窗口的客戶區顯示我們將要打印的內容(GDI繪制函數調用),還有在系統菜單中添加打印功能的菜單項,當用戶點擊打印菜單項,就會執行打印功能。我們先放上應用程序窗口的代碼例子:

#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
BOOL PrintMyPage (HWND) ;
extern HINSTANCE hInst ;//這裏是聲明另一文件的全局變量
extern TCHAR     szAppName[] ;//這裏是聲明另一文件的全局變量
extern TCHAR     szCaption[] ;//這裏是聲明另一文件的全局變量

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;

     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }

     hInst = hInstance ;
     hwnd = CreateWindow (szAppName, szCaption,
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}
//在打印頁或客戶區(為什麽說在客戶區也有繪制?後面你就知道了)繪制圖形和文字
void PageGDICalls (HDC hdcPrn, int cxPage, int cyPage)
{
     static TCHAR szTextStr[] = TEXT ("Hello, Printer!") ;
     Rectangle (hdcPrn, 0, 0, cxPage, cyPage) ;//沿cxPage寬度,cyPage高度的打印頁來繪制矩形
         //在打印頁繪制對角線
     MoveToEx (hdcPrn, 0, 0, NULL) ;
     LineTo   (hdcPrn, cxPage, cyPage) ;
     MoveToEx (hdcPrn, cxPage, 0, NULL) ;
     LineTo   (hdcPrn, 0, cyPage) ;
         //保存當前設備環境,因為等等需要改變映射模式,繪制橢圓和在中心顯示文本
     SaveDC (hdcPrn) ;
     SetMapMode       (hdcPrn, MM_ISOTROPIC) ;
     SetWindowExtEx   (hdcPrn, 1000, 1000, NULL) ;
     SetViewportExtEx (hdcPrn, cxPage / 2, -cyPage / 2, NULL) ;
     SetViewportOrgEx (hdcPrn, cxPage / 2,  cyPage / 2, NULL) ;
     Ellipse (hdcPrn, -500, 500, 500, -500) ;
     SetTextAlign (hdcPrn, TA_BASELINE | TA_CENTER) ;
     TextOut (hdcPrn, 0, 0, szTextStr, lstrlen (szTextStr)) ;
         //恢復到原來的設備環境,那麽剛剛設置的映射模式等都沒效了
     RestoreDC (hdcPrn, -1) ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static int   cxClient, cyClient ;
     HDC          hdc ;
     HMENU        hMenu ;
     PAINTSTRUCT  ps ;
     switch (message)
     {
     case WM_CREATE:
         //獲取系統菜單句柄
          hMenu = GetSystemMenu (hwnd, FALSE) ;
                    //在系統菜單添加打印菜單項
          AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ;
          AppendMenu (hMenu, 0, 1, TEXT ("&Print")) ;
          return 0 ;
     case WM_SIZE:
          cxClient = LOWORD (lParam) ;
          cyClient = HIWORD (lParam) ;
          return 0 ;
     case WM_SYSCOMMAND:
         //當用戶點擊打印菜單項,那麽就會執行PrintMyPage函數來進行打印,PrintMyPage函數返回值是判斷打印是否成功,若失敗則彈出一個錯誤對話框
          if (wParam == 1)
          {
               if (!PrintMyPage (hwnd))
                    MessageBox (hwnd, TEXT ("Could not print page!"),
                                szAppName, MB_OK | MB_ICONEXCLAMATION) ;
               return 0 ;
          }
          break ;
     case WM_PAINT :
          hdc = BeginPaint (hwnd, &ps) ;
                    //Look,我們都知道當生成窗口時,整個客戶區都是無效的,那麽就會發射一條WM_PAINT消息,接著就調用PageGDICalls函數,在客戶區繪制了需要打印的內容
          PageGDICalls (hdc, cxClient, cyClient) ;
          EndPaint (hwnd, &ps) ;
          return 0 ;
     case WM_CLOSE:
        if (IDOK == MessageBox(hwnd, TEXT("是否退出?"), TEXT("對話框"), MB_OKCANCEL | MB_DEFBUTTON1 | MB_ICONQUESTION))
        {
            DestroyWindow(hwnd);
        }
        else
        {
            return 0;
        }
     case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

四、打印功能的實現(即PrintMyPage函數的實現)

到了這裏,我們已經完成了大部分功能,就差最後一個打印功能的函數了,即PrintMyPage函數。
先放代碼上來吧,再進行分析:

#include <windows.h>

HDC  GetPrinterDC (void) ;              
void PageGDICalls (HDC, int, int) ;    
HINSTANCE hInst ;
TCHAR     szAppName[] = TEXT ("Print1") ;//定義全局變量,在上一個文件中有引用
TCHAR     szCaption[] = TEXT ("Print Program 1") ;//定義全局變量,在上一個文件中有引用

BOOL PrintMyPage (HWND hwnd)
{
//DOCINFO結構,第一個字段表明了該結構的大小,第二個字段則是一個值為TEXT ("Print1: Printing")的字符串
     static DOCINFO di = { sizeof (DOCINFO), TEXT ("Print1: Printing")的字符串 } ;
     BOOL           bSuccess = TRUE ;
     HDC            hdcPrn ;
     int            xPage, yPage ;//打印紙的長度和寬度
     if (NULL == (hdcPrn = GetPrinterDC ()))//獲取打印機設備環境
          return FALSE;
     xPage = GetDeviceCaps (hdcPrn, HORZRES) ;
     yPage = GetDeviceCaps (hdcPrn, VERTRES) ;
         /*
         只有StartDoc、StartPage、EndPage函數都成功時,即返回值都大於0時,才能夠調用EndDoc結束文檔
         */
     if (StartDoc (hdcPrn, &di) > 0)//開始新文檔
     {
          if (StartPage (hdcPrn) > 0)//開始新的一頁
          {
                    //GDI繪制命令,GDI模塊將GDI繪制命令存儲在硬盤上的圖元文件
               PageGDICalls (hdcPrn, xPage, yPage) ;

               if (EndPage (hdcPrn) > 0)//在調用EndPage函數後,打印機設備程序將圖元文件轉化為打印輸出,最後將打印輸出存儲為另一個臨時文件
                    EndDoc (hdcPrn) ;//打印結束
               else
                    bSuccess = FALSE ;
          }
     }
     else
          bSuccess = FALSE ;
     DeleteDC (hdcPrn) ;
     return bSuccess ;
}

五、用異常終止過程取消打印

好啦,到目前為止,全部功能基本實現了。可出現了一個問題,如果一個文檔非常大,用戶想打印一頁,但不小心按錯了,變成打印幾百頁了,那怎麽終止打印呢?所以,當應用程序仍在打印時,程序應為用戶提供一個可取消打印作業的便利方法。所以,我們需要修改一下打印功能文件的代碼。如果需要取消一個打印作業,那麽就要調用一個“異常終止過程”,它是一個函數哦。程序員可以把這個函數的地址作為參數傳給SetAbortProc函數(其實這個流程就是註冊一個“異常終止過程”),每當打印時,調用EndPage函數時,就會調用“異常終止過程”來提前判斷是否繼續打印。好的,這裏先上代碼吧。

#include <windows.h>

HDC  GetPrinterDC (void) ;              // in GETPRNDC.C
void PageGDICalls (HDC, int, int) ;     // in PRINT.C
HINSTANCE hInst ;
TCHAR     szAppName[] = TEXT ("Print2") ;
TCHAR     szCaption[] = TEXT ("Print Program 2 (Abort Procedure)") ;
//添加的內容,異常終止過程函數的定義,即在調用EndPage函數時執行的函數
BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode)//參數1是打印機設備環境句柄,如果一切正常,參數2為0,如果由於GDI模塊生成臨時打印輸出文件導致磁盤空間不足,參數2為SP_OUTOFDISK
{
     MSG msg ;
         //看,好像消息循環。沒錯,這裏就是消息循環,不過獲取消息的函數是PeedMessage函數,我們都知道若消息隊列有等待處理的消息,那麽就返回TRUE,若沒有消息,則返回FALSE。我們能註意到,無論該函數怎麽處理,最後始終是返回TRUE,說明打印作業可以繼續,那麽貌似不能達到我們預期的效果(根據用戶的操作,手動取消打印),後面我們會繼續完善,添加打印對話框實現用戶與程序交互。
     while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return TRUE ;
}

BOOL PrintMyPage (HWND hwnd)
{
     static DOCINFO di = { sizeof (DOCINFO), TEXT ("Print2: Printing") } ;
     BOOL           bSuccess = TRUE ;
     HDC            hdcPrn ;
     short          xPage, yPage ;
     if (NULL == (hdcPrn = GetPrinterDC ()))
          return FALSE ;
     xPage = GetDeviceCaps (hdcPrn, HORZRES) ;
     yPage = GetDeviceCaps (hdcPrn, VERTRES) ;
     // 禁止窗口接收鼠標和鍵盤消息,避免重復打印
     EnableWindow (hwnd, FALSE) ;
     SetAbortProc (hdcPrn, AbortProc) ;
     if (StartDoc (hdcPrn, &di) > 0)
     {
          if (StartPage (hdcPrn) > 0)
          {
               PageGDICalls (hdcPrn, xPage, yPage) ;

               if (EndPage (hdcPrn) > 0)
                    EndDoc (hdcPrn) ;
               else
                    bSuccess = FALSE ;
          }
     }
     else
          bSuccess = FALSE ;
     // 啟用窗口接收鼠標鍵盤消息
     EnableWindow (hwnd, TRUE) ;
     DeleteDC (hdcPrn) ;
     return bSuccess ;
}

六、增加一個打印對話框(實現用戶與程序交互)

我們知道上一個代碼的改進存在問題,首先它不直接顯示它是否在打印以及打印何時結束,只有當你用鼠標在程序上移動並發現程序沒有反應時,你才確定它還在處理PrintMyPage例程,即還在打印過程中。我們可以提供一個非模態對話框,還有維護對話框過程。當用戶點擊對話框的Cancel按鈕時,代表用戶想要取消打印,所以程序就終止了打印操作。這個對話框經常被稱為“終止對話框”,該對話框過程經常被稱為“終止對話框過程”。現在,放上改進代碼:

#include <windows.h>

HDC  GetPrinterDC (void) ;              // in GETPRNDC.C
void PageGDICalls (HDC, int, int) ;     // in PRINT.C

HINSTANCE hInst ;
TCHAR     szAppName[] = TEXT ("Print3") ;
TCHAR     szCaption[] = TEXT ("Print Program 3 (Dialog Box)") ;

BOOL bUserAbort ;
HWND hDlgPrint ;

// 打印對話框處理程序
BOOL CALLBACK PrintDlgProc (HWND hDlg, UINT message, 
                            WPARAM wParam, LPARAM lParam)
{
     switch (message)
     {
     case WM_INITDIALOG:
         // 設置窗口標題
          SetWindowText (hDlg, szAppName) ;
          // 停用系統菜單的關閉選項
          EnableMenuItem (GetSystemMenu (hDlg, FALSE), SC_CLOSE, MF_GRAYED) ;
          return TRUE ;

     case WM_COMMAND:  // 按下取消按鈕之後
         // 全局變量,TRUE標識取消按鈕按下
          bUserAbort = TRUE ;
          EnableWindow (GetParent (hDlg), TRUE) ;  // 啟動主窗口
          DestroyWindow (hDlg) ;  // 關閉對話框
          hDlgPrint = NULL ;  // 設定為NULL,防止在消息循環中呼叫IsDialogMessage
          return TRUE ;
     }
     return FALSE ;
}

// 放棄處理程序,用來停止打印
BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode)
{
     MSG msg ;

     while (!bUserAbort && PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
     {
         // IsDialogMessage函數用來將消息發送給非系統模態對話框
          if (!hDlgPrint || !IsDialogMessage (hDlgPrint, &msg))
          {
               TranslateMessage (&msg) ;
               DispatchMessage (&msg) ;
          }
     }
     // 返回TRUE標識繼續打印
     return !bUserAbort ;
}

BOOL PrintMyPage (HWND hwnd)
{
     static DOCINFO di = { sizeof (DOCINFO), TEXT ("Print3: Printing") } ;
     BOOL           bSuccess = TRUE ;
     HDC            hdcPrn ;
     int            xPage, yPage ;

     if (NULL == (hdcPrn = GetPrinterDC ()))
          return FALSE ;
     xPage = GetDeviceCaps (hdcPrn, HORZRES) ;
     yPage = GetDeviceCaps (hdcPrn, VERTRES) ;

     EnableWindow (hwnd, FALSE) ;

     // 先設置用戶取消狀態為False
     bUserAbort = FALSE ;
     // 設置彈窗回調函數
     hDlgPrint = CreateDialog (hInst, TEXT ("PrintDlgBox"), 
                               hwnd, PrintDlgProc) ;
     // 設置放棄處理程序回調函數
     SetAbortProc (hdcPrn, AbortProc) ;

     if (StartDoc (hdcPrn, &di) > 0)
     {
          if (StartPage (hdcPrn) > 0)
          {
               PageGDICalls (hdcPrn, xPage, yPage) ;

               if (EndPage (hdcPrn) > 0)
                    EndDoc (hdcPrn) ;
               else
                    bSuccess = FALSE ;
          }
     }
     else
          bSuccess = FALSE ;
     if (!bUserAbort)
     {
         // 如果用戶沒有取消打印,就重新啟用主窗口,並清除打印對話框
          EnableWindow (hwnd, TRUE) ;
          DestroyWindow (hDlgPrint) ;
     }
     DeleteDC (hdcPrn) ;
     // bUserAbort可以告訴您使用者是否終止了打印作業
     // bSuccess會告訴您是否出了故障
     return bSuccess && !bUserAbort ;
}

Windows程序設計核心總結(打印機-2018.5.5)