Отправляет email-рассылки с помощью сервиса Sendsay

Программирование на Visual С++

  Все выпуски  

Программирование на Visual С++ - No.43 (Свойства в С++, буфер обмена)


Служба Рассылок Subscribe.Ru проекта Citycat.Ru

ПРОГРАММИРОВАНИЕ НА VISUAL C++

Выпуск No. 43 от 6 мая 2001 г.

Здравствуйте, уважаемые подписчики!

Технические работы на сайте закончились и теперь форум и поиск снова работают нормально, причем форум очень сильно изменился соответственно вашим пожеланиям! Можете зайти и посмотреть сами на RSDN.RU.

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Свойства в С++

Автор: Денис Майдыковский

Версия текста: 1.1

Эта статья написана по материалам дискуссии в конференции RU.VISUAL.CPP сети FIDO. Основой примера шаблона свойства послужило письмо от "Vyacheslav V. Lanovets" от 6 декабря 2000г.

Развитие современных систем программирования в направлении объектно-ориентированного и компонентно-ориентированного, а также визуального программирования привело к тому, что многие распространённые и хорошо известные алгоритмические языки перестали соответствовать потребностям современного рынка средств разработки. Одним из таких "нововведений" стали свойства объекта. В общем случае, свойство это пара функций, одна из которых отвечает за установку некоторого значения, а другая за его считывание. Такое решение позволяет, во-первых, обеспечить инкапсуляцию данных и защиту их от неправомерного доступа, а во-вторых, обеспечить целостность данных. Необходимость использования свойств возникает тогда, когда при изменении некоторого параметра требуется произвести ещё некоторые действия.

В тех языках программирования, синтаксис которых находится под контролем создателей (таких как "Visual Basic" или "Delphi") концепция свойств реализована на уровне синтаксиса. В частности, обращение к свойствам объекта производится оператором присваивания, как при обращении к переменной-члену класса в C++. Однако, не стоит обольщаться по поводу простоты синтаксиса. Не стоит забывать, что в простейшем выражении типа

theObject.theProperty = theValue

производится неявный вызов функции.

К сожалению, строгий и стандартизированный язык C++ не позволяет добавлять в его синтаксис столь революционные новации. Какие же возможности предоставляет разработчику C++?

Наиболее простой и самый распространённый способ обеспечения инкапсуляции в C++ заключается в написании пары функций типа get_Value() и put_Value() для каждого параметра. Заметим, что именно так реализованы свойства в технологии Automation. При использовании этого способа можно написать примерно такой класс:

class CValue
{
private:
    int m_value;
public:
    int get_Value()
    {
        return m_value;    // Или более сложная логика
    }
    void put_Value(int value)
    {
        m_value = value;     // Или более сложная логика
    }
};

В этом случае для обращения к такому "свойству" программист должен написать вызов соответствующей функции.

Хорошо это или плохо, но современные средства разработки "приучили" многих к использованию свойств в операторах присваивания и вообще обращению с ними, как с переменными-членами. Учитывая это, разработчики Microsoft Visual C++ добавили в синтаксис языка несколько "Microsoft Specific" конструкций. В частности, модификатор __declspec получил дополнительный параметр "property". Теперь в классе можно объявить "виртуальную" переменную и связать её с соответствующими функциями. Теперь класс может выглядеть примерно так:

class CValue
{
private:
    int m_value;
public:
    __declspec(property(get=get_Value, put=put_Value)) int Value;
    int get_Value()
    {
        return m_value;     // Или более сложная логика
    }
    void put_Value(int value)
    {
        m_value = value;     // Или более сложная логика
    }
};

Строчка сразу за "public:" объявляет "виртуальную" переменную типа int, при обращении к которой фактически будут вызваться функции. С этим классом можно будет работать примерно так:

CValue val;
val.Value = 50;        // На самом деле вызов put_Value()
int z = val.Value;    // На самом деле вызов get_Value()

Чем не "настоящие" свойства? Однако следует заметить, что модификатор __declspec(property) был введён не для повседневного использования, а для встроенной в компилятор поддержки технологии СОМ. Дело в том, что директива импорта библиотеки типа (что бы знать, что это такое, читайте книжки по COM) #import заставляет компилятор VC автоматически генерировать вспомогательные классы-обёртки для объектов СОМ. Вот в этих "автоматических" классах модификатор __declspec(property) используется достаточно широко для максимальной приближенности к синтаксису Visual Basic'а. Приближенность к синтаксису VB достигает такой степени, что свойства сделаны индексными. Для этого, достаточно поставить квадратные скобки после объявления "виртуальной переменной":

__declspec(property(get=get_Value, put=put_Value)) int Value [];

После такого объявления свойство "Value" может принимать один или несколько параметров-индексов, передаваемых в квадратных скобках. Так, например, вызов

Val.Value[f]["two"] = 10;

Будет преобразован в вызов функции

Val.put_Value(f, "two", 10);

Главным недостатком описанного выше способа использования свойств в C++ является его зависимость от компилятора, пресловутая "Microsoft Specific". Впрочем, другой, не менее известный компилятор "Borland C++ Builder" реализует концепцию свойств далёким от стандарта способом. В любом случае часто требуется (или хочется) достичь независимости от компилятора и соответствия кода программы стандарту C++. Что же делать? Оказывается язык C++ позволяет реализовать концепцию свойств в стиле Visual Basic'а. Для этого необходимо воспользоваться шаблонами и перекрыть операторы присваивания и приведения типа. Но для начала необходимо провести некоторую подготовительную работу:

// Базовый класс шаблона свойства.
template <typename proptype, typename propowner> class property
{
protected:
    typedef proptype (propowner::*getter)();
    typedefvoid (propowner::*setter)(proptype);
    propowner * m_owner;
    getter m_getter;
    setter m_setter;
public:
    // Оператор приведения типа. Реализует свойство для чтения.
    operator proptype()
    {
        // Здесь может быть проверка "m_owner" и "m_getter" на NULL
        return (m_owner->*m_getter)();
    }
    // Оператор присваивания. Реализует свойство для записи.
    voidoperator =(proptype data)
    {
        // Здесь может быть проверка "m_owner" и "m_setter" на NULL
        (m_owner->*m_setter)(data);
    }
    // Конструктор по умолчанию.
    property() :
        m_owner(NULL),
        m_getter(NULL),
        m_setter(NULL)
    {
    }
    //Конструктор инициализации.
    property(propowner * const owner, getter getmethod, setter setmethod) :
        m_owner(owner),
        m_getter(getmethod),
        m_setter(setmethod)
    {
    }
    // Инициализация
    void init(propowner * const owner, getter getmethod, setter setmethod)
    {
        m_owner = owner;
        m_getter = getmethod;
        m_setter = setmethod;
    }
};

Теперь класс, реализующий свойство можно написать так:

class CValue
{
private:
    int m_value;
    int get_Value()
    {
        return m_value;    // Или более сложная логика
    }
    void put_Value(int value)
    {
        m_value = value;     // Или более сложная логика
    }
public:
    property <int, CValue> Value;
    CValue()
    {
        Value.init(this, get_Value, put_Value);
    }
};

А вот код, использующий этот класс:

CValue val;

/*
Здесь вызывается оператор присваивания переменной-члена val.Value,
и, следовательно, функция val.put_Value()
*/
val.Value = 50;

/*
Здесь вызывается оператор приведения типа переменной-члена val.Value,
и, следовательно, функция val.get_Value()
*/
int z = val.Value;

Как можно видеть, получились "настоящие" свойства средствами только стандартного синтаксиса С++. Однако, описанный метод не лишен недостатков:

  • При каждом обращении к "свойству" происходит два вызова функции.
  • Использование таких "свойств" требует дополнительных затрат памяти из-за того, что на каждое "свойство" требуется 3 дополнительных указателя, что составляет 12 байт накладных расходов.
  • Использование шаблонов приводит к увеличению размеров исполняемого кода, поскольку компилятор будет генерировать отдельный класс для каждой пары "proptype" и "propowner".
  • Для каждого "свойства" необходимо не забыть произвести инициализацию в конструкторе класса-владельца.

/ / / / ВОПРОС-ОТВЕТ / / / / / / / / / / / / / / / /

Как научить программу реагировать на изменение содержимого буфера обмена?

Автор: Александр Шаргин

Версия текста: 1.0

Программа-пример CbView

Программа-пример MfcCbView

В Windows существует понятие наблюдателя за буфером обмена (clipboard viewer), которым может стать любое окно. Наблюдатель получает от системы уведомления об изменении содержимого буфера обмена в виде сообщения WM_DRAWCLIPBOARD. Соответственно, в ответ на это сообщение программа может загрузить содержимое буфера обмена и выполнить с ним нужные операции (типичный пример - отобразить содержимое буфера обмена в окне).

Интересен способ взаимодействия системы с несколькими наблюдателями за буфером обмена. Дело в том, что с точки зрения Windows наблюдатель всегда один (он называется текущим), и только ему посылаются уведомления. Передача этих уведомлений дальше по цепочке наблюдаетей - задача приложения. Для этого каждая программа, регистрирующая наблюдателя за буфером обмена, получает и сохраняет в переменной HWND предыдущего наблюдателя, а затем передаёт ему сообщения с помощью одной из функций SendMessage, PostMessage и т. п. "Недобросовестная" программа, которая не передаёт уведомления дальше по цепочке, может нарушить работу других приложений и даже других экземпляров самой себя, поэтому писать такие программы настоятельно не рекомендуется.

Рассмотрим процесс работы программы-наблюдателя более подробно. Первое, что ей необходимо сделать - это зарегистрировать своё окно при помощи функции SetClipboardViewer, которая возвращает хэндл текущего наблюдателя и делает текущим наше окно. Как уже говорилось, переданный нам хэндл окна следует сохранить в переменной для дальнейшего использования. Например:


BOOL CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    ...
    static HWND hNextViewer;
    ...
    hNextViewer = SetClipboardViewer(hDlg);
    ...
}

Следующий шаг - научить программу реагировать на сообщение WM_DRAWCLIPBOARD. Это очень простое сообщение, никак не использующее параметры wParam и lParam. Как я уже говорил, программа обязана передать это сообщение дальше по цепочке наблюдателей. Выглядит это так.

case WM_DRAWCLIPBOARD:
    // Работаем с буфером обменаif(IsWindow(hNextViewer))
        PostMessage(hNextViewer, msg, wParam, lParam);
ПРИМЕЧАНИЕ
В общем случае весьма опасно использовать SendMessage для отправки сообщений чужим окнам. Если приложение, которому принадлежит окно, занято выполнением длительной операции или же просто "зависло" в бесконечном цикле, то в ожидании возврата из SendMessage наше приложение зависнет тоже. Вот почему лучше использовать PostMessage, как это сделано в примере выше, или воспользоваться функциями типа SendMessageTimeout.

Не менее важно правильно удалить себя из очереди наблюдателей за буфером обмена. Для этого используется функция ChangeClipboardChain, которая получает в качестве параметров хэндлы нашего окна и следующего наблюдателя в цепочке, которому мы передаём уведомления. Например:


ChangeClipboardChain(hDlg, hNextViewer);

Функция ChangeClipboardChain посылает текущему наблюдателю сообщение WM_CHANGECBCHAIN, передавая полученные хэндлы через параметры wParam и lParam. Текущий наблюдатель сравнивает wParam с хэндлом следующего наблюдателя, который он хранит в переменной. Если обнаружено совпадение, то он просто сохраняет в качестве HWND следующего наблюдателя значение lParam, тем самым удаляя окно нашего приложения из списка. В противном случае он должен передать WM_CHANGECBCHAIN дальше по цепочке. Вот как выглядит типичный обработчик сообщения WM_CHANGECBCHAIN:

case WM_CHANGECBCHAIN:
        if(hNextViewer == (HWND)wParam)
            hNextViewer = (HWND)lParam;
        elseif(IsWindow(hNextViewer))
            PostMessage(hNextViewer, msg, wParam, lParam);

Пример CbView иллюстрирует все принципы, которые мы только что рассмотрели. Программа CbView добавляет своё окно в цепочку наблюдателей за буфером обмена, когда пользователь устанавливает галочку "Spy clipboard". В ответ на WM_DRAWCLIPBOARD она проверяет содержимое буфера обмена, и если это текст (формат CF_TEXT), загружает его в RichEdit.

В MFC наблюдение за буфером обмена осуществляется по тому же самому принципу. В ней предусмотрены макросы ON_WM_DRAWCLIPBOARD и ON_WM_CHANGECBCHAIN для добавления соответствующих обработчиков в карту сообщений, а также функции SetClipboardViewer и ChangeClipboardChain класса CWnd, соответствующие одноимённым функциям из Win32 API. Программа-пример MfcCbView демонстрирует создание наблюдателя за буфером обмена с использованием MFC.

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Все на сегодня. До следующих встреч!

Алекс Jenter   jenter@rsdn.ru
Красноярск, 2001. Рассылка является частью проекта RSDN.

Предыдущие выпуски     Статистика рассылки


RLE Banner Network

http://subscribe.ru/
E-mail: ask@subscribe.ru

В избранное