Hypertextový odkaz v dialogu

Při dokončování projektů často programátoři přidávají do aplikace tzv. "About" - dialog se stručnými informacemi o aplikaci a jejím autorovi. Často řeší problém, jak umístit na dialog aktivní hypertextový odkaz, který reaguje na kliknutí myši. Tento článek poradí, jak na to.

Co bude hypertextový odkaz umět ?

Nejdříve si shrneme požadavky, které bude hypertextový odkaz splňovat

Návrh dialogu

Výchozí podoba tlačítka Výchozím typem ovládacího prvku bude tlačítko (button), který obsahuje základní funcionalitu, požadovanou od hypertextového odkazu, tedy kliknutí myší. Zároveň bude možné "kliknout" i pomocí klávesnice, stejně jako běžné tlačítko. V resource editoru na dialog umístíme tlačítko, jehož rozměry budou o něco větší než je velikost textu hypertextového odkazu (viz vlevo). Velikost tlačítka určuje aktivní oblast, která bude reagovat na kliknutí. V nastavení tlačítka zvolte vlastnost "owner draw", která nám umožní nakreslit text hypertextového odkazu podle naší vůle. Text tlačítka by měl obsahovat buďto URL adresu, nebo e-mailovou adresu.

Inicializace komponent

Při obsluze zprávy WM_INITDIALOG je nutné dialog předpřipravit pro správnou funkci. Pro vytvoření podtrženého fontu je potřeba nahradit font tlačítka nově vytvořeným fontem. Font bude ve většině případů identický s původním fontem, s výjimkou podtržení. Dalším bodem je načtení kurzoru myši "pacičky", který bude zobrazen při ukázání kurzorem myši. K dosažení této vlastnosti bude nutné tlačítko subclassovat a zachytávat zprávu WM_SETCURSOR. Uvedenou funkcionalitu obsahuje tento kód funkce InitURLButton, který by měla být volána z funkce OnInitDialog (ať již děláte program v MFC nebo v API):

static WNDPROC OldWindowProc;
static HCURSOR hCursor = NULL;
static LRESULT CALLBACK SetCursorProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    if(msg == WM_SETCURSOR)
    {
        SetCursor(hCursor);
        return 0;
    }
    return CallWindowProc(OldWindowProc, hwnd, msg, wParam, lParam);
}

BOOL IsURLButton(HWND hWnd)
{
    TCHAR szClassName[0x20];
    DWORD dwStyle;
    
    szClassName[0] = 0;
    GetClassName(hWnd, szClassName, (sizeof(szClassName) / sizeof(TCHAR)) - 1);
    dwStyle = GetWindowLong(hWnd, GWL_STYLE);

    // Must be a button
    if(_tcsicmp(szClassName, _T("Button")))
        return FALSE;
        
    // Must not be a groupbox
    if((dwStyle & BS_GROUPBOX) == BS_GROUPBOX)
        return FALSE;

    // If ownerdrawn, the OK.
    return ((dwStyle & BS_OWNERDRAW) == BS_OWNERDRAW) ? TRUE : FALSE;
}

// This function sets the font for one URL button.
// It must be a button with owner-draw style set.
int InitURLButton(HWND hDlg, UINT nIDCtrl, BOOL bBigFont)
{
    LOGFONT logFont;
    HFONT   hFont = NULL;
    HWND    hWnd = GetDlgItem(hDlg, nIDCtrl);

    // If the button is not there, do nothing.
    if(hWnd == NULL)
        return ERROR_FILE_NOT_FOUND;

    // Retrieve the settings of the dialog font.
    // The buttons should have the same font like the dialog font.
    hFont = (HFONT)SendMessage(hDlg, WM_GETFONT, 0, 0);
    GetObject(hFont, sizeof(LOGFONT), &logFont);

    // Setup the font
    logFont.lfUnderline = TRUE;
    if(bBigFont != FALSE)
    {
        logFont.lfHeight = 18;
        logFont.lfWeight = FW_BOLD;
    }

    // Create the font
    hFont = CreateFontIndirect(&logFont);
    if(hFont == NULL)
        return GetLastError();

    // Load URL-Point cursor for this buttons
    hCursor = LoadCursor(hInst, MAKEINTRESOURCE(IDC_URLPOINT));
    if(hCursor == NULL)
        return GetLastError();

    if(IsURLButton(hWnd))
    {
        // Set the new window font and procedure
        SendMessage(hWnd, WM_SETFONT, (LPARAM)hFont, 0);
        OldWindowProc = (WNDPROC)SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)SetCursorProc);
    }
    return ERROR_SUCCESS;
}

Aby kód fungoval, musíte do resourců vložit kurzor IDC_URLPOINT, který se dá načíst z knihovny mshtml.dll. Protože ale má pokaždé jiný identifikátor, je lepší jej vložit do programu, abychom měli jistotu, že kód bude fungovat na všech verzích Windows.

Vykreslení

Protože jsme na dialog přidali tlačítko se stylem "owner draw", budeme si jej muset sami nakreslit. Před nakreslením tlačítka přijde do dialogu zpráva WM_DRAWITEM. Pokud je projekt vytvořen v čistém API, přidejte další case do funkce DialogProc:

int WINAPI DialogProc01(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch(uMsg)
    {
        ...

        case WM_DRAWITEM:
            DrawURLButton(hDlg, (LPDRAWITEMSTRUCT)lParam);
            break;

        ...
    }
    return FALSE;
}    

Pokud je projekt vytvořen v MFC, přidejte do mapy zpráv položku pro WM_DRAWITEM:

BEGIN_MESSAGE_MAP(TAboutDlg, CDialog)
    ON_WM_DRAWITEM()
END_MESSAGE_MAP()

V obsluze této zprávy musíte vykreslit text tlačítka. Barva textu se bude lišit podle toho, zda je právě stlačeno tlačítko myši. Verze pro API:

// Draws hypertext button. It is drawn with the blue color
// when normal and with the red color when clicked.
BOOL DrawURLButton(HWND hDlg, LPDRAWITEMSTRUCT dis)
{
    TCHAR szText[240];
    UINT  length = 0;

    if(dis == NULL)
        return ERROR_INVALID_PARAMETER;

    if(dis->CtlType == ODT_BUTTON)
    {
        // Retrieve text from button
        length = GetDlgItemText(hDlg, dis->CtlID, szText, (sizeof(szText) / sizeof(TCHAR)) - 1);
        
        // Select color and draw it.
        if(dis->itemState & ODS_SELECTED)
            SetTextColor(dis->hDC, RGB(0xFF, 0x00, 0x00));
        else
            SetTextColor(dis->hDC, RGB(0x00, 0x00, 0xFF));
        TextOut(dis->hDC, 0, 0, szText, length);
    }

    return TRUE;
}

a verze pro MFC:

// Draw hypertext button. It is drawn with the blue color,
// when normal and with the red color when clicked.
int TAboutDlg::DrawURLButton(LPDRAWITEMSTRUCT dis)
{
    TCHAR szText[240];
    UINT  length = 0;

    if(dis == NULL)
        return ERROR_INVALID_PARAMETER;

    if(dis->CtlType == ODT_BUTTON)
    {
        // Retrieve text from button
        length = GetDlgItemText(dis->CtlID, szText, _tsize(szText));
        
        // Select color and draw it.
        if(dis->itemState & ODS_SELECTED)
            SetTextColor(dis->hDC, RGB(0xFF, 0x00, 0x00));
        else
            SetTextColor(dis->hDC, RGB(0x00, 0x00, 0xFF));
        TextOut(dis->hDC, 0, 0, szText, length);
    }

    return ERROR_SUCCESS;
}

Obsluha kliknutí na odkaz

Při obsluze našeho "WWW" tlačítka postupujeme podobně jako při obsluze kteréhokoliv jiného tlačítka. Při jeho stlačení přijde do obslužné rutiny dialogu zpráva WM_COMMAND s parametry ID tlačítka a kódem zprávy BN_CLICKED. V reakci na tuto zprávu zjistíme text tlačítka a použijeme jej jako parametr funkce ShellExecute, která zajistí buďto spuštění aktuálně nainstalovaného webového prohlížeče nebo mailového klienta:

// This callback handles the mouse click on an URL button (WWW, mail, http).
// It invokes the default web browser/mail client for the button
// (Recognized from button text)
BOOL ClickURLButton(HWND hURL)
{
    HCURSOR hWaitCursor = LoadCursor(hInst, MAKEINTRESOURCE(IDC_URLWAIT));
    TCHAR   szUrl[128] = _T("");
    TCHAR   szText[128];

    // Retrieve button text
    GetWindowText(hURL, szText, (sizeof(szText) / sizeof(TCHAR)) - 1);

    // Test E-mail addresses. They must have "mailto:" at the begin
    if(_tcschr(szText, _T('@')) != NULL)
    {
        _tcscpy(szUrl, _T("mailto:"));
        _tcscat(szUrl, szText);
    }

    // Test "http://"
    if(!_tcsnicmp(szText, _T("http://"), 7))
        _tcscpy(szUrl, szText);

    // If only "www." at the begin, we have to add "http://"
    if(!_tcsnicmp(szText, _T("www."), 4))
    {
        _tcscpy(szUrl, _T("http://"));
        _tcscat(szUrl, szText);
    }

    // If any valid URL address, invoke the default application
    if(szUrl[0] != 0)
    {
        // Set waiting cursor and launch default browser
        SetCursor(hWaitCursor);
        ShellExecute(NULL, _T("open"), szUrl, NULL, NULL, SW_SHOWNORMAL);
        SetCursor(hCursor);
    }
    return TRUE;
}

Jakmile program přeložíme a spustíme, bude hypertextový odkaz vypadat takto:

Výsledný dialog

Výsledný příklad si můžete stáhnout a vyzkoušet. Příklad je psaný v API, programátoři by jej měli být schopni přepsat tak, aby se dal zařadit do projektu napsaného i v MFC.