"AND" "OR"
Главная Информер Журнал Форум

Работа с буфером обмена.

Реализация команд Cut, Copy, и Paste

В этом разделе приводится пример, реализующий эти команды используя два формата буфера обмена: CF_OWNERDISPLAY и CF_TEXT. Обычно, перед тем, как информация будет скопирована в буфер обмена (clipboard), пользователь должен выделить определённый кусок текста. Для этого в приложении должны быть реализованы все возможности. После того, как текст будет выделен, необходимо реализовать всплывающее меню по правой кнопке мыши, а так же акселераторы к пунктам этого меню.

Чтобы сделать всплывающее меню, приложение должно обработать сообщение WM_INITMENUPOPUP:

case WM_INITMENUPOPUP:
    InitMenu((HMENU) wParam);
    break;

Функция InitMenu позволяет сделать определённые пункты меню доступными либо недоступными (серыми) и выглядит следующим образом:

Пример:

void WINAPI InitMenu(HMENU hmenu)
{
    int  cMenuItems = GetMenuItemCount(hmenu);
    int  nPos;
    UINT id;
    UINT fuFlags;
    PLABELBOX pbox = (hwndSelected == NULL) ? NULL :
        (PLABELBOX) GetWindowLong(hwndSelected, 0);
 
    for (nPos = 0; nPos < cMenuItems; nPos++) 
    {
        id = GetMenuItemID(hmenu, nPos);

        switch (id)
        {
            case IDM_CUT:
            case IDM_COPY:
            case IDM_DELETE:
                if (pbox == NULL || !pbox->fSelected)
                    fuFlags = MF_BYCOMMAND | MF_GRAYED;
                else if (pbox->fEdit)
                    fuFlags = (id != IDM_DELETE && pbox->ichSel
                            == pbox->ichCaret) ?
                        MF_BYCOMMAND | MF_GRAYED :
                        MF_BYCOMMAND | MF_ENABLED;
                else
                    fuFlags = MF_BYCOMMAND | MF_ENABLED;

                EnableMenuItem(hmenu, id, fuFlags);
                break;

            case IDM_PASTE:
                if (pbox != NULL && pbox->fEdit)
                    EnableMenuItem(hmenu, id,
                        IsClipboardFormatAvailable(CF_TEXT) ?
                            MF_BYCOMMAND | MF_ENABLED :
                            MF_BYCOMMAND | MF_GRAYED
                    );
                else
                    EnableMenuItem(hmenu, id,
                        IsClipboardFormatAvailable(
                                uLabelFormat) ?
                            MF_BYCOMMAND | MF_ENABLED :
                            MF_BYCOMMAND | MF_GRAYED
                    );

        }
    }
}

Далее, для того, чтобы обрабатывать команды меню, необходимо добавить в приложение обработку команды WM_COMMAND в главную оконную процедуру:

Пример:

case WM_COMMAND:
    switch (LOWORD(wParam))
    {
        case IDM_CUT:
            if (EditCopy())
                EditDelete();
            break;

        case IDM_COPY:
            EditCopy();
            break;

        case IDM_PASTE:
            EditPaste();
            break;

        case IDM_DELETE:
            EditDelete();
            break;

        case IDM_EXIT:
            DestroyWindow(hwnd);
    }
    break;

Для реализации команд Copy и Cut используется функция EditCopy (См. Копирование данных в буфер обмена). Для реализации команды Paste используется функция EditPaste (См. Вставка данных из буфера обмена).

Копирование информации в буфер обмена

Процесс копирования информации в буфер обмена осуществляется следующим образом:

  1. Открываем буфер обмена функцией OpenClipboard.
  2. Очищаем буфер обмена функцией EmptyClipboard.
  3. Вызываем функцию SetClipboardData для каждого формата буфера обмена, которые поддерживает приложение.
  4. Закрываем буфер обмена функцией CloseClipboard.

Для того чтобы скопировать выделенный текст, используется следующая структура:

#define BOX_ELLIPSE  0
#define BOX_RECT     1

#define CCH_MAXLABEL 80
#define CX_MARGIN    12

typedef struct tagLABELBOX {
    RECT rcText;    // координаты прямоугольника с текстом
    BOOL fSelected; // TRUE если выделен label
    BOOL fEdit;     // TRUE если текст выделен
    int nType;      // прямоугольное или овальное
    int ichCaret;   // позиция каретки
    int ichSel;     // with ichCaret, delimits selection
    int nXCaret;    // window position corresponding to ichCaret
    int nXSel;      // window position corresponding to ichSel
    int cchLabel;   // длина текста в atchLabel
    TCHAR atchLabel[CCH_MAXLABEL];
} LABELBOX, *PLABELBOX;

А вот сама функция EditCopy:

Пример:

BOOL WINAPI EditCopy(VOID)
{
    PLABELBOX pbox;
    LPTSTR  lptstrCopy;
    HGLOBAL hglbCopy;
    int ich1, ich2, cch;

    if (hwndSelected == NULL)
        return FALSE;

    // Открываем буфер обмена и очищаем его.

    if (!OpenClipboard(hwndMain))
        return FALSE;
    EmptyClipboard();

    // Получаем указатель на структуру LABELBOX.
 
    pbox = (PLABELBOX) GetWindowLong(hwndSelected, 0);

    // Если текст выделен, то копируем его используя формат CF_TEXT.

    if (pbox->fEdit)
    {
        if (pbox->ichSel == pbox->ichCaret)     // нулевая длина
        {   
            CloseClipboard();                   // выделение
            return FALSE;
        }
 
        if (pbox->ichSel < pbox->ichCaret)
        {
            ich1 = pbox->ichSel;
            ich2 = pbox->ichCaret;
        }
        else
        {
            ich1 = pbox->ichCaret;
            ich2 = pbox->ichSel;
        }
        cch = ich2 - ich1;

        // Выделяем память для текста.

        hglbCopy = GlobalAlloc(GMEM_MOVEABLE,
            (cch + 1) * sizeof(TCHAR));
        if (hglbCopy == NULL)
        {
            CloseClipboard();
            return FALSE;
        }

        // Блокируем хэндл и копируем текст в буфер.
 
        lptstrCopy = GlobalLock(hglbCopy);
        memcpy(lptstrCopy, &pbox->atchLabel[ich1],
            cch * sizeof(TCHAR));
        lptstrCopy[cch] = (TCHAR) 0;    // нулевой символ
        GlobalUnlock(hglbCopy);

        // Помещаем хэндл в буфер обмена.

        SetClipboardData(CF_TEXT, hglbCopy);
    }

    // Если текст не выделен, то копируем весь label.

    else
    {
        // Сохраняем копию выделенного лабела в локальной памяти.
        // С ней мы будем работать и освобождать в ответ на
        // сообщение WM_DESTROYCLIPBOARD.

        pboxLocalClip = (PLABELBOX) LocalAlloc(
            LMEM_FIXED,
            sizeof(LABELBOX)
        );
        if (pboxLocalClip == NULL)
        {
            CloseClipboard();
            return FALSE;
        }
        memcpy(pboxLocalClip, pbox, sizeof(LABELBOX));
        pboxLocalClip->fSelected = FALSE;
        pboxLocalClip->fEdit = FALSE;

        // Помещаем в буфер обмена данные трёх форматов.

        SetClipboardData(uLabelFormat, NULL);
        SetClipboardData(CF_OWNERDISPLAY, NULL);
        SetClipboardData(CF_TEXT, NULL);
    }

    // Закрываем буфер обмена.

    CloseClipboard();

    return TRUE;
}

Вставка данных из буфера обмена

Процесс вставки информации из буфера обмена осуществляется следующим образом:

  1. Открываем буфер обмена функцией OpenClipboard.
  2. Определяем форматы данных, хранящихся в буфере обмена.
  3. Получаем хэндл данных нужного формата при помощи функции GetClipboardData.
  4. Вставляем копию данных в документ.

    Владельцем хэндла возвращённого GetClipboardData остаётся всё ещё буфер обмена, поэтому приложение не должно его освобождать.

  5. Закрываем буфер обмена функцией CloseClipboard.

Определение структуры LABELBOX:

#define BOX_ELLIPSE  0 
#define BOX_RECT     1 
 
#define CCH_MAXLABEL 80 
#define CX_MARGIN    12 
 
typedef struct tagLABELBOX {
    RECT rcText;    // координаты прямоугольника с текстом
    BOOL fSelected; // TRUE если будет копироваться весь label
    BOOL fEdit;     // TRUE если текст выделен
    int nType;      // прямоугольное или овальное
    int ichCaret;   // позиция каретки
    int ichSel;     // with ichCaret, delimits selection
    int nXCaret;    // window position corresponding to ichCaret
    int nXSel;      // window position corresponding to ichSel
    int cchLabel;   // длина текста в atchLabel
    TCHAR atchLabel[CCH_MAXLABEL];
} LABELBOX, *PLABELBOX;

Далее следует исходник функции EditPaste.

пример:

VOID WINAPI EditPaste(VOID)
{
    PLABELBOX pbox;
    HGLOBAL   hglb;
    LPTSTR    lptstr;
    PLABELBOX pboxCopy;
    int cx, cy;
    HWND hwnd;

    pbox = hwndSelected == NULL ? NULL :
        (PLABELBOX) GetWindowLong(hwndSelected, 0);
 
    // Если приложение находится в режиме редактирования,
    // то получаем текст из буфера обмена.

    if (pbox != NULL && pbox->fEdit)
    {
        if (!IsClipboardFormatAvailable(CF_TEXT))
            return;
        if (!OpenClipboard(hwndMain))
            return;

        hglb = GetClipboardData(CF_TEXT);
        if (hglb != NULL)
        {
            lptstr = GlobalLock(hglb);
            if (lptstr != NULL)
            {
                // Функция ReplaceSelection вставляет текст
                // и перерисовывает окно.

                ReplaceSelection(hwndSelected, pbox, lptstr);
                GlobalUnlock(hglb);
            }
        }
        CloseClipboard();

        return;
    }

    // Если приложение не находится в режиме редактирования,
    // то создаём окно label-а.

    if (!IsClipboardFormatAvailable(uLabelFormat))
        return;
    if (!OpenClipboard(hwndMain))
        return;

    hglb = GetClipboardData(uLabelFormat);
    if (hglb != NULL)
    {
        pboxCopy = GlobalLock(hglb);
        if (pboxCopy != NULL)
        {
            cx = pboxCopy->rcText.right + CX_MARGIN;
            cy = pboxCopy->rcText.top * 2 + cyText;

            hwnd = CreateWindowEx(
                WS_EX_NOPARENTNOTIFY | WS_EX_TRANSPARENT,
                atchClassChild, NULL, WS_CHILD, 0, 0, cx, cy,
                hwndMain, NULL, hinst, NULL
            );
            if (hwnd != NULL)
            {
                pbox = (PLABELBOX) GetWindowLong(hwnd, 0);
                memcpy(pbox, pboxCopy, sizeof(LABELBOX));
                ShowWindow(hwnd, SW_SHOWNORMAL);
                SetFocus(hwnd);
            }
            GlobalUnlock(hglb);
        }
    }
    CloseClipboard();
}

Как зарегистрировать формат буфера обмена

Для этого используется функция RegisterClipboardFormat, которая обычно вызывается при инициализации приложения.

// atchTemp может содержать имя формата
// и завершаться нулевым символом.
//
LoadString(hinstCurrent, IDS_FORMATNAME, atchTemp,
    sizeof(atchTemp)/sizeof(TCHAR));
uLabelFormat = RegisterClipboardFormat(atchTemp);
if (uLabelFormat == 0)
    return FALSE;

Обработка сообщений WM_RENDERFORMAT и WM_RENDERALLFORMATS

Если не известно, какого формата данные будут помещены в буфер обмена, то можно передать в функцию SetClipboardData хэндл равный NULL, при этом приложение сгенерирует сообщение WM_RENDERFORMAT либо WM_RENDERALLFORMATS и Вы должны позаботиться, чтобы обработать эти сообщения. В данном случае перед вызовом функции SetClipboardData нельзя открывать буфер обмена.

Сообщения WM_RENDERFORMAT и WM_RENDERALLFORMATS обрабатываются следующим образом:

case WM_RENDERFORMAT:
    RenderFormat((UINT) wParam);
    break;

case WM_RENDERALLFORMATS:
    RenderFormat(uLabelFormat);
    RenderFormat(CF_TEXT);
    break;

Код функции RenderFormat представлен ниже.

Как обычно, определение структуры LABELBOX:

#define BOX_ELLIPSE  0 
#define BOX_RECT     1 
 
#define CCH_MAXLABEL 80 
#define CX_MARGIN    12 
 
typedef struct tagLABELBOX {
    RECT rcText;    // координаты прямоугольника с текстом
    BOOL fSelected; // TRUE если будет копироваться весь label
    BOOL fEdit;     // TRUE если текст выделен
    int nType;      // прямоугольное или овальное
    int ichCaret;   // позиция каретки
    int ichSel;     // with ichCaret, delimits selection
    int nXCaret;    // window position corresponding to ichCaret
    int nXSel;      // window position corresponding to ichSel
    int cchLabel;   // длина текста в atchLabel
    TCHAR atchLabel[CCH_MAXLABEL]; 
} LABELBOX, *PLABELBOX;

Пример:

void WINAPI RenderFormat(UINT uFormat)
{
    HGLOBAL hglb;
    PLABELBOX pbox;
    LPTSTR  lptstr;
    int cch;

    if (pboxLocalClip == NULL)
        return;

    if (uFormat == CF_TEXT)
    {
        // Выделяем буфер для текста.

        cch = pboxLocalClip->cchLabel;
        hglb = GlobalAlloc(GMEM_MOVEABLE,
            (cch + 1) * sizeof(TCHAR));
        if (hglb == NULL)
            return;

        // Копируем текст из pboxLocalClip.

        lptstr = GlobalLock(hglb);
        memcpy(lptstr, pboxLocalClip->atchLabel,
            cch * sizeof(TCHAR));
        lptstr[cch] = (TCHAR) 0;
        GlobalUnlock(hglb);

        // Помещаем хэндл в буфер обмена.

        SetClipboardData(CF_TEXT, hglb);
    }
    else if (uFormat == uLabelFormat)
    {
        hglb = GlobalAlloc(GMEM_MOVEABLE, sizeof(LABELBOX));
        if (hglb == NULL)
            return;
        pbox = GlobalLock(hglb);
        memcpy(pbox, pboxLocalClip, sizeof(LABELBOX));
        GlobalUnlock(hglb);

        SetClipboardData(uLabelFormat, hglb);
    }
}

Обработка сообщения WM_DESTROYCLIPBOARD

Чтобы освободить различные ресурсы, можно включить в приложение обработку сообщения WM_DESTROYCLIPBOARD. Например, при копировании label-а в буфер обмена, выделяется локальная память. Эту память можно освободить при обработке сообщения WM_DESTROYCLIPBOARD.

case WM_DESTROYCLIPBOARD:
    if (pboxLocalClip != NULL)
    {
        LocalFree(pboxLocalClip);
        pboxLocalClip = NULL;
    }
    break;

Использование формата буфера обмена CF_OWNERDISPLAY

Если Вы помещаете данные в буфер обмена, используя формат CF_OWNERDISPLAY, то необходимо проделать следующее:

  • Обработать сообщение WM_PAINTCLIPBOARD. Это сообщение посылается владельцу буфера обмена, когда его "окно" должно быть перерисовано.
  • Обработать сообщение WM_SIZECLIPBOARD. Это сообщение посылается владельцу буфера обмена, когда размеры его "окна" изменились либо изменилось его содержимое.

    Обычно при этом, устанавливается положение скрола и диапазон скроллирования окна буфера обмена.

  • Обработать сообщения WM_HSCROLLCLIPBOARD и WM_VSCROLLCLIPBOARD. Эти сообщения посылаются владельцу буфера обмена, когда "окно" буфера обмена было проскроллировано.
  • Обработать сообщение WM_ASKCBFORMATNAME. Это сообщение посылает буфер обмена приложению, чтобы узнать формат.

Ниже представлена оконная процедура, с обработкой этих сообщений.

Пример:

LRESULT CALLBACK MainWindowProc(hwnd, msg, wParam, lParam)
HWND hwnd;
UINT msg;
WPARAM wParam;
LPARAM lParam;
{
    static RECT rcViewer;

    RECT rc;
    LPRECT lprc;
    LPPAINTSTRUCT lpps;

    switch (msg)
    {
        //
        // Обрабатываем другие сообщения.
        //
        case WM_PAINTCLIPBOARD: 
            // Определяем размер лабела.
 
            SetRect(&rc, 0, 0, 
                pboxLocalClip->rcText.right + CX_MARGIN,
                pboxLocalClip->rcText.top * 2 + cyText
            );

            // Центрируем картинку в окне буфера обмена.

            if (rc.right < rcViewer.right)
            {
                rc.left = (rcViewer.right - rc.right) / 2;
                rc.right += rc.left;
            }
            if (rc.bottom < rcViewer.bottom)
            {
                rc.top = (rcViewer.bottom - rc.bottom) / 2;
                rc.bottom += rc.top;
            }

            // Рисуем изображение, используя структуру PAINTSTRUCT.

            lpps = (LPPAINTSTRUCT) GlobalLock((HGLOBAL) lParam);
            PaintLabel(lpps, pboxLocalClip, &rc);
            GlobalUnlock((HGLOBAL) lParam);
            break;

        case WM_SIZECLIPBOARD:
            // Записываем размеры окна в статической
            // структуре RECT.

            lprc = (LPRECT) GlobalLock((HGLOBAL) lParam);
            memcpy(&rcViewer, lprc, sizeof(RECT));
            GlobalUnlock((HGLOBAL) lParam);

            // Устанавливаем диапазон скроллирования в ноль.

            SetScrollRange((HWND) wParam, SB_HORZ, 0, 0, TRUE);
            SetScrollRange((HWND) wParam, SB_VERT, 0, 0, TRUE);

            break;

        case WM_ASKCBFORMATNAME:
            LoadString(hinst, IDS_OWNERDISPLAY,
                (LPSTR) lParam, wParam);
            break;

        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}