Использование стандартного диалога настроек порта




Не всегда настройку порта можно жестко зашить в код программы. Внешние устройства могут позволять изменять параметры линии связи, чаще всего скорость обмена, которая зависит от длины соединительного кабеля. В таких случаях разумно предоставить пользователю самому задавать режимы обмена. Можно самому разработать соответствующий настроечный диалог, а можно воспользоваться стандартным, предоставляемым операционной системой, а точнее, производителем порта. Стандартный диалог выводится функцией CommConfigDialog, которая работает со структурой COMMCONFIG. Как и в случае со структурой DCB, заполнять структуру COMMCONFIG можно вручную или вызовом соответствующих функций.

Структура COMMCONFIG имеет следующие поля:

typedef struct _COMM_CONFIG {

DWORD dwSize;

WORD wVersion;

WORD wReserved;

DCB dcb;

DWORD dwProviderSubType;

DWORD dwProviderOffset;

DWORD dwProviderSize;

WCHAR wcProviderData[1];

} COMMCONFIG, *LPCOMMCONFIG;

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

dwSize

Задает размер структуры COMMCONFIG в байтах

wVersion

Задает номер версии структуры COMMCONFIG. Должен быть равным 1.

wReserved

Зарезервировано и не используется

dcb

Блок управления устройством (DCB) для порта RS-232.

dwProviderSubType

Задает тип устройства и формат устройство-зависимого блока информации. Фактически это тип порта. Конкретные значения данного поля приведены в описании структуры COMMPROP выше.

dwProviderOffset

Смещение, в байтах, до устройство-зависимого блока информации от начала структуры.

dwProviderSize

Размер, в байтах, устройство-зависимого блока информации.

wcProviderData

Устройство-зависимый блок информации. Это поле может быть любого размера или вообще отсутствовать.

Несмотря на то, что нужна только DCB, приходится иметь дело со всеми полями. Заполнение данной структуры противоречивыми данными может привести к неправильной настройке порта, поэтому следует пользоваться функцией GetCommConfig:

BOOL GetCommConfig(

HANDLE hCommDev,

LPCOMMCONFIG lpCC,

LPDWORD lpdwSize

);

Параметры функции следующие:

hCommDev

Дескриптор открытого коммуникационного порта.

lpCC

Адрес выделеного и заполненого нулями, кроме поля dwSize, блока памяти под структуру COMMCONFIG. В поле dwSize нужно занести размер структуры COMMCONFIG. После вызова функции все поля структуры будут содержать информацию о текущих параметрах порта.

lpdwSize

Адрес двойного слова, которое после воврата из функции будет содержать число фактически переданных в структуру байт.

В случае успешного завершения функция возвращает ненулевое значение. Параметры функции CommConfigDialog следующие:

lpszName

Указатель на строку с именем порта, для которого отображается диалоговое окно. К реальному имени порта эта строка не имеет никакого отношения, она просто выводится в заголовке окна.

hWnd

Дескриптор окна, которое владеет данным диалоговым окном. Должен быть передан корректный дескриптор окна-владельца или NULL, если у диалогового окна нет владельца.

lpCC

Указатель на структуру COMMCONFIG. Эта структура содержит начальные установки, используемые для отображения в диалоговом окне, и установленные пользователем изменения, при завершении диалога.

Как и большинство других функций Win32 API, функция CommConfigDialog возвращает отличное от нуля значение, в случае успешного завершения, и нуль, если возникла ошибочная ситуация.

Фактическая настройка порта выполняется функцией SetCommConfig:

BOOL SetCommConfig(

HANDLE hCommDev,

LPCOMMCONFIG lpCC,

DWORD dwSize

);

Параметры имеют то же самое значение, как и в функции GetCommConfig. На рис.2.10 показано соответствие между полями структуры DCB и полями диалогового окна настроек порта.

 

Рис.16.10. Соответствие полей структуры DCB

 

Пример 5. Выполнить задание предыдущего примера, но с возможностью настройки параметров порта через стандартное диалоговое окно.

Пояснение. Порядок действий:

1. Запускаем среду разработки Visual Studio.NET. Открываем проект, созданный в примере 4.

2. Добавляем на форму элемент управления кнопка и изменяем надпись на «Настроить порт» (поле «Caption» на странице свойств) (рис.16.11).

Рис.16.11. Окно программы

3. Добавляем обработчик события нажатия на кнопку «Настроить порт». Обработчик события должен выглядеть следующим образом:

void CCOMTestDlg::OnBnClickedButton3()

{

DWORD sz;

COMMCONFIG comm; // переменная структуры

if(!GetCommConfig(m_hCom,&comm,&sz)) // получение параметров порта

{

MessageBox("Невозможно получить параметры порта!");

return;

}

CommConfigDialog("COM2",NULL,&comm); // вызов диалога настройки порта

if(!SetCommConfig(m_hCom,&comm,sz)) // установка параметров порта

MessageBox("Невозможно установить параметры порта!");

}

После компиляции проекта следует открыть порт. После этого нажатие на кнопку «Настроить порт» приведет к появлению диалогового окна, показанного на рис.16.12.

Рис.16.12. Окно настройки порта

Если нажать на кнопку «Настроить порт», предварительно не открыв порт, появится окно сообщения с надписью "Невозможно получить параметры порта!".

Функция CommConfigDialog не выполняет настройки порта. Она все лишь позволяет пользователю изменить некоторые поля в блоке DCB, содержащемся в структуре COMMCONFIG.

Прием и передача данных

Прием и передача данных выполняется функциями ReadFile и WriteFile, то есть теми же самыми, которые используются для работы с дисковыми файлами. Вот как выглядят прототипы этих функций:

BOOL ReadFile(

HANDLE hFile,

LPVOID lpBuffer,

DWORD nNumOfBytesToRead,

LPDWORD lpNumOfBytesRead,

LPOVERLAPPED lpOverlapped

);

BOOL WriteFile(

HANDLE hFile,

LPVOID lpBuffer,

DWORD nNumOfBytesToWrite,

LPDWORD lpNumOfBytesWritten,

LPOVERLAPPED lpOverlapped

);

Описание параметров функций:

hFile

Дескриптор открытого файла коммуникационного порта.

lpBuffer

Адрес буфера. Для операции записи данные из этого буфера будут передаваться в порт. Для операции чтения в этот буфер будут помещаться принятые данные.

nNumOfBytesToRead, nNumOfBytesToWrite

Число ожидаемых к приему или предназначенных к передаче байт.

nNumOfBytesRead, nNumOfBytesWritten

Число фактически принятых или переданных байт. Если принято или передано меньше данных, чем запрошено, то для дискового файла это свидетельствует об ошибке, а для коммуникационного порта совсем не обязательно. Причина в тайм-аутах.

lpOverlapped

Адрес структуры OVERLAPPED, используемой для асинхронных операций. Для синхронных операций данный параметр должен быть равным NULL.

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

BOOL TransmitCommChar(

HANDLE hFile,

char cChar

);

Данная функция передает один (и только один) внеочередной байт в линию, не смотря на наличие данных в очереди передатчика, и перед этими данными. Первый параметр – дескриптор открытого порта. Второй параметр – символ для передачи.

Пример 6. Микропроцессорная система сбора данных, подключенная к ПК через СОМ-порт, выполняет контроль температуры некоторого объекта. Написать программу, которая принимает значение температуры и отображает ее в виде графика. Для получения данных необходимо отправить запрос – команда 0хBC. Периодичность запроса значения температуры – 100 мс. Скорость передачи составляет 9600 бит/с. Формат посылки – 8 бит данных в информационном блоке, 1 стоповый бит, проверка на чётность отсутствует.

Пояснение. Порядок действий:

1. Запускаем среду разработки Visual Studio.NET. Открываем проект, созданный в примере 5, который берём за основу.

2. Изменяем вид диалогового окна. Выносим на форму элемент управления «Static Text» корректируем его ID на IDC_STATIC3. Также очищаем поле «Caption» чтобы не отображался текст. Затем необходимо растянуть этот элемент управления так, чтобы по ширине он занял всю форму, а по высоте – приблизительно 70%. В поле «Border» на странице свойств установить значение «True». В области, которую занял элемент управления «Static Text» будет отображаться график изменения температуры. Также необходимо привязать к элементу этому управления переменную с именем m_graph. На рис.16.13 приводится вид диалогового окна.

Рис.16.13. Окно программы

Кроме того, необходимо добавить элемент управления «Static Text» для отображения текущей температуры. Изменить ID на IDC_STATIC4. К этому элементу управления привязать переменную с именем m_currtemp.

3. Для хранения полученных данных о температуре используется переменная m_temp и имеющая тип CByteArray. Эта переменная – член класса CCOMTestDlg. Для того, чтобы добавить переменную, в окне «Class View» выделить класс «CCOMTestDlg», вызвать контекстное меню и из подменю «Add» выбрать команду «Add Variable». Появившееся диалоговое окно заполнить так, как показано на рис.16.14.

Рис.16.14. Окно добавление переменной

 

4. Добавляем обработчик события таймера. Для этого из страницы свойств (Properties) вызвать окно «Messages» и напротив позиции «WM_TIMER» выбрать «OnTimer» (рис.16.15).

Рис.16.15 Создание обработчика события таймера

 

5. Добавляем в функцию - обработчик события таймера код, как показано ниже:

void CCOMTestDlg::OnTimer(UINT nIDEvent)

{

TransmitCommChar(m_hCom,(char)0xBC); // передача значения 0xBC

BYTE rdata;

DWORD size;

ReadFile(m_hCom,&rdata,1,&size,NULL);// чтение 1 байта с порта

if(size!=1) // если 1 байт не считан

{

// выдаем сообщение

SetWindowText("Отсутствуют данные о температуре!");

return; // выход из функции

}

m_temp.Add(rdata); // добавляем данные в массив

CString s;

// форматирование строки

s.Format("Текущая температура: %d град.",rdata);

m_currtemp.SetWindowText(s);

DrawGraph(); // обновление графика на экране

CDialog::OnTimer(nIDEvent);

}

В этой функции запрос на получение данных о температуре отправляется с помощью функции TransmitCommChar. Затем происходит считывание с порта 1 байта данных Если данные были получены, значение переменной size (количество считанных байт) будет равно 1. Если же это значение отличается от 1, выдается сообщение о том, что данные о температуре отсутствуют. Благодаря настройкам тайм-аутов, программа не «зависнет» из-за отсутствия принятых данных.

6. Реализуем функцию void DrawGraph(void). Для этого ее необходимо сделать членом класса CCOMTestDlg. Реализация функции имеет следующий вид:

void CCOMTestDlg::DrawGraph(void)

{

CClientDC dc(&m_graph); // создание контекста для рисования

CPen p(PS_SOLID,2,RGB(200,0,0));// создание пера красного цвета

CPen pp(PS_DOT,1,RGB(0,0,0));

CRect rect;

CRgn rgn;

CPen* op;

m_graph.RedrawWindow(); // перерисовка окна рисования графика

m_graph.GetClientRect(&rect);

// получение размеров области рисоания

rgn.CreateRectRgn(0,0,rect.Width(),rect.Height());

// создание региона для ограничения области рисования

op=dc.SelectObject(&pp); // установка пера

for(int x=20;x<rect.Width();x=x+20) // рисование сетки

{

dc.MoveTo(x,0); // перемещение начальной позиции

dc.LineTo(x,rect.Height()); // рисование линии

}

dc.SetTextColor(RGB(50,50,50)); // установка цвета текста

dc.SetBkColor(RGB(210,210,210)); // установка цвета фона

for(int y=20;y<rect.Height();y=y+20)

{

dc.MoveTo(0,rect.Height()-y);

dc.LineTo(rect.Width(),rect.Height()-y);

CString s;

s.Format("%d",y); // форматирование строки

dc.TextOut(rect.Width()-30,rect.Height()-y,s);

// вывод текстовых меток

}

dc.SelectObject(&p); // выбор красного пера в контекст

dc.SelectClipRgn(&rgn); // установка региона

if(rect.Width()<=m_temp.GetCount()) m_temp.RemoveAll();

// если значение элементов в массиве превышает размер области

// рисования, то удаляются все элементы массива

for(int i=1;i<m_temp.GetCount();i++)

{

// рисование графика температуры

dc.MoveTo(i,rect.Height()-m_temp.GetAt(i));

dc.LineTo(i-1,rect.Height()-m_temp.GetAt(i-1));

}

dc.SelectObject(op);

}

7. Корректируем обработчик нажатия на кнопку «Открыть порт». Добавляем в этот обработчик строку

SetTimer(1,200,NULL);

При нажатии на эту кнопку, в случае успешного открытия порта, запускается таймер, который будет срабатывать через каждые 200 мс. При этом будет вызываться функция OnTimer, в которой реализован код по запросу данных о температуре и их отображению.

8. Корректируем обработчик события нажатия на кнопку «Закрыть порт». Поскольку при закрытии порта данные невозможно будет считать, необходимо остановить таймер. Для этого в обработчик добавляем следующую строчку:

KillTimer(1);

9. Откомпилировав проект, появится диалоговое окно, как показано на рис.16.16 (при условии, что данные принимаются через порт).

Рис.16.16. Окно программы

 

Использование потоков

В предыдущем примере функция ReadFile завершала свое выполнение даже если данные не поступали в порт. Путем анализа количества считанных байт можно выяснить причину завершения работы функции (приняты данные, истек тайм-аут). Существует ряд задач, в которых поток данных не является равномерным. Например, пожарная сигнализация. При нормальной обстановке в компьютер данные могут не поступать, а обрабатываться на локальных объектах. В случае пожара на главный компьютер передаются данные о месте возникновения пожара, а также другая информация. Для реализации такой задачи использование циклического опроса порта будет не эффективным. В этом случае более выгодно использовать потоки.

Каждый поток начинает выполнение с некоторой входной функции. Для создания вторичного (рабочего, фонового) потока, в нем тоже должна быть входная функция, прототип которой которая выглядит так:

DWORD WINAPI ThreadFunction(PVOID pvParam){... return(0); }

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

BOOL SetCommMask(

HANDLE hFile,

DWORD dwEvtMask

);

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

EV_BREAK - Состояние разрыва приемной линии

EV_CTS - Изменение состояния линии CTS

EV_DSR - Изменение состояния линии DSR

EV_ERR - Ошибка четности

EV_RING - Входящий звонок на модем (сигнал на линии RI порта)

EV_RLSD - Изменение состояния линии RLSD (DCD)

EV_RXCHAR - Символ принят и помещен в приемный буфер

EV_RXFLAG - Принят символ заданный полем EvtChar структуры DCB использованной для настройки режимов работы порта

EV_TXEMPTY - Из буфера передачи передан последний символ

Если dwEvtMask равно нулю, то отслеживание событий запрещается.

Разумеется, всегда можно получить текущую маску отслеживаемых событий с помощью функции

BOOL GetCommMask(

HANDLE hFile,

LPDWORD lpEvtMask

);

Вторым параметром задается адрес переменной принимающей значение текущей установленной маски отслеживаемых событий.

Когда маска отслеживаемых событий задана, можно приостановить выполнение программы до наступления события. При этом программа не будет занимать процессор. Это выполняется вызовом функции

BOOL WaitCommEvent(

HANDLE hFile,

LPDWORD lpEvtMask,

LPOVERLAPPED lpOverlapped,

);

где hFile – дескриптор открытого порта. В переменной, адресуемой вторым параметром lpEvtMask, в единичное состояние установятся только те биты, которые соответствуют реально произошедшим событиям.

Адрес структуры OVERLAPPED (третий параметр функции – lpOverlapped) требуется для асинхронного ожидания. Для синхронных операций этот параметр должен быть NULL.

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

LRESULT SendMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);

Эта функция WinAPI посылает определенное сообщение окну или окнам. Функция вызывает оконную процедуру (WndProc) определенного окна и не выходит из процедуры, пока та не обработает сообщение. Другими словами, выполнение программы не будет продолжено, пока сообщение не будет обработано.

hWnd - дескриптор окна, которое получает сообщение;

Msg - определенное сообщение;

wParam - содержит определенную информацию о сообщении (зависит от сообщения);

lParam - содержит определенную информацию о сообщении (зависит от сообщения).

Возвращаемое значение функции зависит от типа посылаемого сообщения.

Если диалоговое окно получает сообщение, вызывается определенная функция. Какая именно функция будет вызвана, указывается в карте сообщений (Message Map). Прототип функции – обработчика пользовательского сообщения имеет вид:

LRESULT OnMyMessage(WPARAM wp, LPARAM lp);

Каждое сообщение имеет свой номер. Некоторые из сообщений заняты операционной системой, поэтому их нельзя использовать. Сообщения с номерами больше чем WM_USER не задействованы, поэтому их можно использовать.

Пример 7. Написать программу обмена данными между двумя компьютерами по СОМ-порту (чат). Скорость обмена данными – 9600 бит/с.

Пояснение. Порядок действий:

  1. Запускаем среду разработки Visual Studio.NET. Открываем проект, созданный в примере 4.
  2. Добавляем на форму следующие элементы управления: Edit Control, Button, List Box, а также Static Text, и располагаем их на диалоговом окне так, как показано на рис.16.17.

Рис.16.17. Спроектированное диалоговое окно

В элементе управления Edit Control пользователь будет вводить текстовое сообщение. При нажатии на кнопку «Отправить» введенное сообщение будет передано в порт. Принимаемые сообщения будут отображаться в элементе управления «Список» (List Box). Для этого элемента управления на странице свойств в поле «Sort» установить значение «False». В противном случае строки, добавляемые в элемент управления, будут отсортированы по алфавиту, и порядок приема сообщений не будет сохраняться.

  1. Связываем с элементами управления переменные. К «List Box» привязывается переменная с именем m_recmess, а к текстовому полю «Edit Control» переменная с именем m_sendmess. Для выполнения этих действий необходимо по очереди выделить каждый элемент управления и вызвать контекстное меню, из которого выбрать команду «Add Variable» (рис.16.18).

Рис.16.18. Добавление переменной

  1. Создаем глобальную переменную hCom. В этой переменной будет храниться дескриптор открытого порта. Поскольку переменная глобальная, доступ к ней возможен из функции потока. Также необходимо определить пользовательское сообщение. Для этого в начале файла COMTestDlg.cpp, после секции #include, добавляем строки:

volatile HANDLE hCom; // глобальная переменная – дескриптор

#define WM_MESSRECIEVED WM_USER+1 // пользовательское сообщение

Для остановки работы потока необходимо знать указатель на него. В библиотеке MFC для работы с потоками используется класс CWinThread. В класс CCOMTestDlg необходимо добавить переменную-член класса:

CWinThread* m_thread;

При создании потока в этой переменной будет храниться указатель на него.

  1. Добавляем функцию рабочего потока, в котором будет ожидаться прием сообщения. Эта функция не является членом класса диалогового окна CCOMTestDlg. Ее описание следует расположить в файле COMTestDlg.cpp, перед началом описания обработчиков событий нажатия на кнопку. Функция выглядит следующим образом:

UINT ThreadFunction(LPVOID param)

{

DWORD cm;

SetCommMask(hCom, EV_RXCHAR); // установка маски событий порта

PurgeComm(hCom, PURGE_RXCLEAR | PURGE_TXCLEAR);

// очистка очередей приема и передачи

WaitCommEvent(hCom, &cm, NULL); // ожидание события

::SendMessage((HWND)param, WM_MESSRECIEVED, 0, 0);

// посылка сообщения главному окну программы

return 0;

}

  1. Изменяем обработчик события нажатия на кнопку «Открыть порт». В случае успешного открытия порта, глобальной переменной hCom необходимо присвоить дескриптор открытого порта. Кроме того, необходимо запустить поток. Обработчик события нажатия на кнопку «Открыть порт» имеет вид:

void CCOMTestDlg::OnBnClickedButton1()

{

CString sp;

m_port.GetLBText(m_port.GetCurSel(),sp);

m_hCom=CreateFile(sp,GENERIC_READ | GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL);

// открываем последовательный порт

if(m_hCom==INVALID_HANDLE_VALUE)

{

m_status.SetWindowText("Невозможно открыть порт!");

// если невозможно открыть порт, выдаем сообщение

return;

}

else // если порт открыт

m_status.SetWindowText("Порт открыт.");

// сообщаем, что порт открыт

m_bOpen.EnableWindow(0); // изменение состояний

m_bClose.EnableWindow(1); // элементов управления

m_port.EnableWindow(0);

 

}

DCB *pDCB; // указатель на структуру DCB

COMMTIMEOUTS ct; // переменная для настройки тайм-аутов

pDCB=new DCB;

memset(pDCB,0,sizeof(DCB)); // обнуление полей структуры

if(!GetCommState(m_hCom,pDCB)) // получение состояния порта

{

MessageBox("Невозможно получить параметры порта!");

return;

}

if(!GetCommTimeouts(m_hCom, &ct)) // получение тайм-аутов

{

MessageBox("Невозможно получить значения тайм-аутов!");

return;

}

pDCB->DCBlength=sizeof(DCB); // размер структуры

pDCB->BaudRate=CBR_9600; // скорость передачи данных

pDCB->ByteSize=8; // размер слова

pDCB->StopBits=ONESTOPBIT; // количество стоповых бит

pDCB->Parity=NOPARITY; // четность отсутствует

ct.ReadIntervalTimeout=10; // значение тайм-аута чтения

ct.ReadTotalTimeoutConstant=300;

ct.ReadTotalTimeoutMultiplier=2;

ct.WriteTotalTimeoutConstant=300; // значение тайм-аута записи

ct.WriteTotalTimeoutMultiplier=2;

if(! SetCommState(m_hCom,pDCB)) // установка параметров порта

{

MessageBox("Невозможно установить параметры порта!");

return;

}

if(!SetCommTimeouts(m_hCom,&ct)) // установка тайм-аутов

{

MessageBox("Невозможно установить значения тайм-аутов!");

return;

}

hCom=m_hCom;

// присвоение глобальной переменной дескриптора порта

m_thread=AfxBeginThread(ThreadFunction, m_hWnd, THREAD_PRIORITY_NORMAL); // запуск рабочего потока

}

  1. Изменяем обработчик события нажатия на кнопку «Закрыть порт». При нажатии на кнопку необходимо остановить выполнение потока. Обработчик события нажатия на кнопку «Закрыть порт» имеет вид:

void CCOMTestDlg::OnBnClickedButton2()

{

::TerminateThread(m_thread->m_hThread,0); // остановка потока

CloseHandle(m_hCom); // закрытие порта

m_status.SetWindowText("Порт закрыт.");

m_port.EnableWindow(1);

// изменение состояний элементов управления

m_port.EnableWindow(1);

m_bOpen.EnableWindow(1);

m_bClose.EnableWindow(0);

}

  1. Создаем функцию – обработчик сообщения WM_MESSRECIEVED, которое передается главному окну программы из рабочего потока при приеме данных. Эта функция является членом класса диалогового окна. В окне «Class View» следует выделить класс CCOMTestDlg и из контекстного меню выбрать команду «Add Function». Появится диалоговое окно, в котором необходимо указать название функции, а также тип и имена параметров, передаваемых в функцию. Вид этого окна представлен на рис.16.19.

Рис.16.19. Окно добавления функции в класс

 

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

BEGIN_MESSAGE_MAP(CCOMTestDlg, CDialog)

ON_WM_SYSCOMMAND()

ON_WM_PAINT()

ON_WM_QUERYDRAGICON()

//}}AFX_MSG_MAP

ON_BN_CLICKED(IDC_BUTTON1, OnBnClickedButton1)

ON_BN_CLICKED(IDC_BUTTON2, OnBnClickedButton2)



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2016-04-12 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: