• Thứ Ba, 08/03/2011 14:57 (GMT+7)

    Chương trình đồng hồ quả lắc

    Nguyễn Văn Hiệp
    Để viết chương trình thể hiện đồng hồ quả lắc, bạn cần thực hiện một số công việc sau:

    - Tìm hiểu và nắm vững cách thức hiển thị các dữ liệu lên cửa sổ đồ họa của ứng dụng trên Windows. Các dữ liệu cần hiển thị của 1 ứng dụng chỉ thuộc 1 trong 3 loại: chuỗi văn bản, ảnh bitmap, hình toán học như đoạn thẳng, chữ nhật, vòng tròn… Qui trình hiển thị chuỗi văn bản gồm 3 bước chính: Tạo font chữ cần dùng, đăng ký font cho Windows dùng, gọi hàm TextOut() để xuất chuỗi ra cửa sổ. Qui trình hiển thị ảnh bitmap gồm 2 bước chính: nạp ảnh từ file vào bộ nhớ (đối tượng BITMAP), gọi hàm BitBlt() để copy 1 phần hay toàn bộ ảnh gốc và dán vào vùng hiển thị của cửa sổ. Qui trình hiển thị các hình toán học gồm 3 bước chính: Tạo đối tượng Pen miêu tả nét vẽ và màu vẽ đường viền; Tạo đối tượng Brush miêu tả mẫu tô và màu tô phần diện tích bên trong hình cần vẽ; Gọi hàm vẽ để vẽ hình cần vẽ (hàm Lineto để vẽ đoạn thằng, hàm Rectangle để vẽ khối chữ nhật, hàm Ellipse để vẽ khối tròn hay ellipse,…).

    - Tạo timer đếm thời gian với chu kỳ đếm đủ nhỏ, mỗi lần đếm xong chu kỳ xác đính, Timer sẽ kích hoạt hàm xử lý sự kiện Timeout, ta sẽ hiển thị lại các chi tiết của đồng hồ tại thời điểm này.

    - Phân tích các thành phần cần vẽ của đồng hồ để lập trình vẽ chúng: Tốt nhất là dùng 1 trình soạn thảo ảnh vẽ ảnh bitmap miêu tả các phần tử cố định theo thời gian của đồng hồ như mặt đồng hồ, các mốc thời gian trên mặt đồng hồ,… để hiển thị chúng dễ dàng và nhanh chóng khi cần thiết. Còn các phần tử thay đổi động theo thời gian như kim giờ/phút/giây và dây quả lắc thì dùng các hàm vẽ hình toán học vẽ chúng theo từng thời điểm.

    Để thấy rõ ràng và cụ thể các ý tưởng nêu trên, chúng tôi giới thiệu qui trình điển hình để xây dựng ứng dụng bằng VC++ thể hiện 1 đồng hồ quả lắc đơn giản (Hình 1).

    1. Dùng tiện ích soạn thảo đồ họa (thí dụ Paint) để vẽ trực quan mặt đồng hồ theo ý muốn rồi lưu lên file đồ họa ở thư mục xác định (thí dụ c:\dongho.bmp) (Hình 2):

    2. Chạy VC++ (hoặc bằng icon shortcut trên màn hình desktop hoặc bằng menu Start.Programs...). Tạo Project quản lý ứng dụng bằng cách chọn menu File.New..., khi cửa sổ New hiển thị, chọn loại project "MFC AppWizard (exe)", chọn vị trí thư mục chứa project, nhập tên Project (thí dụ Dongho) rồi ấn button OK.

    3. Trong cửa sổ Wizard - Step 1, chọn checkbox Dialog Based, rồi ấn button Finish để hoàn thành việc đặc tả các thông số miêu tả Project.

    4. Khi cửa sổ thiết kế Form ứng dụng hiển thị, chọn từng đối tượng giao diện được tạo sẵn rồi xóa chúng để tạo form trống hoàn toàn (mọi chi tiết trong cửa sổ đều được vẽ động tại thời điểm chạy).

    5. Chọn menu View.Classwizard để hiển thị cửa sổ Classwizard, chọn tag Message Maps, chọn mục CDonghoDlg (mục duy nhất trong listbox Object IDs), duyệt tìm và chọn mục WM_TIMER trong listbox Messages, nhấn vào button Add Function để tạo hàm xử lý sự kiện timeout của timer, click vào button Edit

    Code để hiển thị cửa sổ soạn code cho hàm này rồi viết code cho hàm này như sau:

    void CDonghoDlg::OnTimer(UINT nIDEvent) {
    //xóa timer
    KillTimer(IDT_TIMER1);
    //xác định vùng cần vẽ lại trong cửa sổ
    RECT rect;
    rect.left =13; rect.top =13;
    rect.right = bm.bmWidth; rect.bottom = bm.bmHeight;
    //yêu cầu Window vẽ lại vùng cần vẽ lại
    InvalidateRect(&rect);
    CDialog::OnTimer(nIDEvent);
    }

    6. Duyệt tìm hàm OnPaint() rồi hiệu chỉnh thân của hàm hày thành:

    void CTimduongDlg::OnPaint() {
    CPaintDC dc(this); // device context for painting
    if (IsIconic()) {
    SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);
    // Center icon in client rectangle
    int cxIcon = GetSystemMetrics(SM_CXICON);
    int cyIcon = GetSystemMetrics(SM_CYICON);
    CRect rect;
    GetClientRect(&rect);
    int x = (rect.Width() - cxIcon + 1) / 2;
    int y = (rect.Height() - cyIcon + 1) / 2;
    // Draw the icon
    dc.DrawIcon(x, y, m_hIcon);
    } else {
    //phần viết mới bắt đầu từ đây
    //gọi cha làm trước
    CDialog::OnPaint();
    //hiển thị lại mặt đồng hồ
    HDC hDC,hdcMemory;
    hDC = dc.m_hDC;
    hdcMemory = CreateCompatibleDC(hDC);
    hbmpOld = (HBITMAP)SelectObject(hdcMemory, (HBITMAP)hbmp);
    BitBlt(hDC,10,10, bm.bmWidth, bm.bmHeight, hdcMemory,0,0, SRCCOPY);
    SelectObject(hdcMemory, hbmpOld);
    DeleteDC(hdcMemory);

    HPEN hpen, hpenOld;
    HBRUSH hbrush, hbrushOld;
    int xo,yo,rql,rh,rm, rs;
    int x, y;
    //thiết lập tâm đồng hồ
    xo = 80; yo = 80;
    //thiết lập bán kính quả lắc, kim giờ/phút/giây
    rql = 140; rh = 50; rm = 55; rs = 60;
    //tạo pen để vẽ dây quả lắc
    hpen = CreatePen(PS_SOLID, 2, RGB(0,0, 255));
    //đăng ký cho Windows dùng
    hpenOld = (HPEN)dc.SelectObject(hpen);
    DeleteObject(hpenOld);
    //tạo brush để tô nền quả lắc
    hbrush = CreateSolidBrush(RGB(255, 0, 0));
    //đăng ký cho Windows dùng
    hbrushOld = (HBRUSH)dc.SelectObject(hbrush);
    DeleteObject(hbrushOld);
    //xác định giờ/phút/giây hiện hành
    SYSTEMTIME now;
    GetLocalTime(&now);
    //tính góc của dây lắc (chọn góc quay max. là 40 độ)
    double goc = 80*now.wMilliseconds/1000;
    if (goc < 40) goc = goc +70;
    else goc = 150-goc;
    //đổi góc của dây lắc từ độ ra radian
    goc = goc*3.1416/180;
    //xác định tâm quả lắc
    x = xo+rql*cos(goc);
    y = yo+rql*sin(goc);
    //vẽ dây lắc
    dc.MoveTo (xo,yo);
    dc.LineTo(x,y);
    //vẽ quả lắc
    dc.Ellipse(x-5,y-5,x+5,y+5);
    //tạo pen để vẽ kim giờ
    hpen = CreatePen(PS_SOLID, 3, RGB(0,0,0));
    //đăng ký cho Windows dùng
    hpenOld = (HPEN)dc.SelectObject(hpen);
    DeleteObject(hpenOld);
    //tính góc của kim giờ
    goc = 90+360*(now.wHour+(double)now.wMinute/60)/12;
    //đổi góc từ độ ra radian
    goc = goc*3.1416/180;
    //xác định tọa độ đỉnh thứ 2 của kim giờ
    x = xo-rh*cos(goc);
    y = yo-rh*sin(goc);
    //vẽ kim giờ
    dc.MoveTo (xo,yo);
    dc.LineTo(x,y);
    //tạo pen để vẽ kim phút
    hpen = CreatePen(PS_SOLID, 2, RGB(65,110,55));
    //đăng ký cho Windows dùng
    hpenOld = (HPEN)dc.SelectObject(hpen);
    DeleteObject(hpenOld);
    //tính góc của kim phút
    goc = 90+360*now.wMinute/60;
    //đổi góc từ độ ra radian
    goc = goc*3.1416/180;
    //xác định tọa độ đỉnh thứ 2 của kim phút
    x = xo-rm*cos(goc);
    y = yo-rm*sin(goc);
    //vẽ kim phút
    dc.MoveTo (xo,yo);
    dc.LineTo(x,y);
    //tạo pen để vẽ kim giây
    hpen = CreatePen(PS_SOLID, 1, RGB(237,5,220));
    //đăng ký cho Windows dùng
    hpenOld = (HPEN)dc.SelectObject(hpen);
    DeleteObject(hpenOld);
    //tính góc của kim giây
    goc = 90+360*now.wSecond/60;
    //đổi góc từ độ ra radian
    goc = goc*3.1416/180;
    //xác định tọa độ đỉnh thứ 2 của kim giây
    x = xo-rs*cos(goc);
    y = yo-rs*sin(goc);
    //vẽ kim giây
    dc.MoveTo (xo,yo);
    dc.LineTo(x,y);
    //tạo chuỗi miêu tả giờ/phút/giây hiện hành
    sprintf(buf,"%.2d:%.2d:%.2d",now.wHour,now.wMinute,now.wSecond);
    //đăng ký font cho Windows dùng
    hFontOld = (HFONT)dc.SelectObject((HFONT)hFont);
    //thiết lập các chế độ hiển thị chuỗi
    dc.SetBkMode(TRANSPARENT);
    dc.SetTextColor(RGB(0,0,0));
    dc. SetTextAlign(TA_CENTER);
    dc.TextOut(bm.bmWidth/2+10,bm.bmHeight-20,buf);
    //kích hoạt timer đếm 50ms
    SetTimer(IDT_TIMER1,50,(TIMERPROC)NULL);
    }
    }
    }

    7. Duyệt tìm hàm OnInitDialog() rồi thêm vào cuối hàm (ngay trước lệnh return TRUE;) đoạn code khởi động như sau:

    // TODO: Add extra initialization here
    //thiết lập font để hiển thị chuỗi
    StrFont.lfEscapement = 0;
    StrFont.lfOrientation = 0;
    StrFont.lfWeight = FW_REGULAR;
    StrFont.lfItalic =0;
    StrFont.lfUnderline = 0;
    StrFont.lfStrikeOut = 0;
    StrFont.lfCharSet = ANSI_CHARSET;
    StrFont.lfOutPrecision = OUT_DEFAULT_PRECIS;
    StrFont.lfClipPrecision = CLIP_DEFAULT_PRECIS;
    StrFont.lfQuality = PROOF_QUALITY;
    StrFont.lfPitchAndFamily = VARIABLE_PITCH | FF_ROMAN;
    strcpy(StrFont.lfFaceName,"Times New Roman");
    StrFont.lfHeight = 20;
    StrFont.lfWidth = 10;
    //tạo đối tượng font để hiển thị chuỗi
    hFont = CreateFontIndirect(&StrFont);
    //load ảnh bitmap miêu tả nền đồng hồ
    hbmp = (HBITMAP)LoadImage(NULL, "c:\\dongho.bmp", IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION | LR_DEFAULTSIZE | LR_LOADFROMFILE);
    if (hbmp==0) {
    ::MessageBox(NULL,"Không có file bitmap c:\\dongho.bmp","",MB_OK);
    return;
    }
    //tìm thông tin về bitmap
    GetObject(hbmp, sizeof(BITMAP), &bm);
    //hiệu chỉnh kích thước cửa sổ theo kích thước đồng hồ
    this->MoveWindow (0,0,bm.bmWidth+24,bm.bmHeight+50);
    //kích hoạt timer đếm 50ms
    SetTimer(IDT_TIMER1,50,(TIMERPROC) NULL);
    return TRUE;

    8. Dời về đầu file, ngay trước lệnh đặc tả class CAboutDlg, viết đoạn code định nghĩa hằng, biến cần dùng như sau:
    #define IDT_TIMER1 1001
    HFONT hFont,hFontOld;
    LOGFONT StrFont;
    HBITMAP hbmp,hbmpOld;
    BITMAP bm;
    char buf[50];

    9. Thêm lệnh sau vào ngay sau danh sách các lệnh include ở đầu file:
    #include <math.h>

    10. Chọn menu Build.Execute Dongho.exe để dịch và chạy ứng dụng. Nếu bạn thực hiện đúng mọi bước trên thì chương trình không có lỗi và sẽ chạy tốt. Cửa sổ ứng dụng đúng theo đặc tả ban đầu.

    Bạn có thể truy cập website của tạp chí để copy file Dongho.zip chứa toàn bộ Project VC++ của chương trình.

    ID: A1012_122