|
РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ. |
Приветствую вас!
/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /
Отчёты Crystal Reports для Visual C++ 6
Недавно я начал писать один небольшой проект на VC с отчетом Crystal Reports 8 и столкнулся со следующей проблемой: я не знал, как написать отчет. После поиска материалов на эту тему в интернете, у меня сложилось впечатление, что перед разработчиками на VC не стоит проблема создания отчетов. На CodeGuru в разделе Databases я не нашел ни одного материала на эту тему. Пришлось копать эту тему самому. К сожалению, у меня оказался только один пример, в котором довольно сложный отчет полностью создаётся в run-time без использования редактора отчетов. Это автоматически означало, что мне нужно будет изучить несколько десятков, а то и сотен килобайт текста, прежде чем я выдам первый отчет. Времени на это у меня не было. Поэтому для создания отчета я воспользовался следующей технологией, которая и описывается ниже.
Для выполнения этого проекта необходимо:
Приступим.
Для начала, создадим наш отчет. Запускаем Crystal Report Designer. Создаем blank report. Добавляем ODBC connection, указывающее, на пример, на БД pubs на вашем SQL сервере, или на какую-нибудь таблицу в mdb-файле. Выбираем таблицу pubs.dbo.authors, давим add кнопку, закрываем окно. В появившемся окне дизайнера отчетов перетаскиваем в область Details нужные поля: au_id, au_fname, au_lname. Сохраняем отчёт.
Создаём простой Dialog-based проект со всеми настройками по умолчанию. В меню Projects->Add to project->Components and controls добавляем Crystal Report Viewer Control. В окне Confirm classes давим OK. Закрываем окно Components and controls. Добавляем Crystal Report Viewer Control на диалог. В окне ClassWizard для диалога добавляем обработчик WM_SHOWWINDOW. At the Member variables tab добавляем переменную m_CRView1. В начало файла SampRepDlg.cpp добавляем строки
#import <craxdrt.tlb> no_namespace
#import <msado15.dll> rename ("EOF", "adoEOF")
(подразумевается, что файл craxdrt.tlb находится в одной из стандартных папок для include. Изначально он находится в каталоге C:\Program Files\Seagate Software\Crystal Reports\Developer Files\include\)
так же добавляем следующие строки в начале файла RepSampDlg.cpp
const CLSID CLSID_Application =
{0xb4741fd0,0x45a6,0x11d1,{0xab,0xec,0x00,0xa0,0xc9,0x27,0x4b,0x91}};
const IID IID_IApplication =
{0x0bac5cf2,0x44c9,0x11d1,{0xab,0xec,0x00,0xa0,0xc9,0x27,0x4b,0x91}};
const CLSID CLSID_ReportObjects =
{0xb4741e60,0x45a6,0x11d1,{0xab,0xec,0x00,0xa0,0xc9,0x27,0x4b,0x91}};
const IID IID_IReportObjects =
{0x0bac59b2,0x44c9,0x11d1,{0xab,0xec,0x00,0xa0,0xc9,0x27,0x4b,0x91}};
Переходим к обработчику CRepSampDlg::OnShowWindow. Я обычно создаю стандартное окружение для работы с COM-объектами:
try{
}
catch(const _com_error& e)
{
_bstr_t bstrSource(e.Source());
_bstr_t bstrDescription(e.Description());
CString strError;
strError.Format("_com_error catched at
CRepSampDlg::OnShowWindow\n"
"Source : %s\nDescription : %s",
(LPCSTR)bstrSource,(LPCSTR)bstrDescription);
AfxMessageBox(strError);
}
В try-блоке присоединяем наш файл отчета:
HRESULT hr=S_OK;
IApplicationPtr pApp;
IReportPtr pRep;
hr=CoCreateInstance(CLSID_Application, NULL, CLSCTX_INPROC_SERVER , IID_IApplication,
(void **) &pApp);
if(FAILED(hr)) _com_issue_error(hr);
pRep = pApp->OpenReport(_bstr_t("d:\\projects\\RepSamp\\Report1.rpt"));
m_CRView1.SetReportSource(pRep);
m_CRView1.ViewReport();
Собираем проект, и запускаем. Появится отчет, который в качестве источника данных использует свои настройки по умолчанию. Теперь давайте подставим ему в качестве источника данных необходимый нам Recordset. Я предпочитаю ADO. Следующий код я добавил сразу после строки "HRESULT hr=S_OK;" :
ADODB::_ConnectionPtr pConn;
pConn.CreateInstance(__uuidof(ADODB::Connection));
if(FAILED(hr)) _com_issue_error(hr);
CString sConnStr("Provider=SQLOLEDB.1;"
"Integrated Security=SSPI;Persist Security Info= False;"
"Initial Catalog= pubs;Data Source= DATACENTER");
hr= pConn->Open(_bstr_t(sConnStr),_bstr_t(L""), _bstr_t(L""),
ADODB::adConnectUnspecified);
if(FAILED(hr)) _com_issue_error(hr);
ADODB::_RecordsetPtr pRs;
pRs.CreateInstance(__uuidof(ADODB::Recordset));
CString sSQL("SELECT * FROM authors");
pRs->Open(_bstr_t(sSQL), pConn.GetInterfacePtr(), ADODB::adOpenDynamic,
ADODB::adLockOptimistic,ADODB::adCmdText);
if(FAILED(hr)) _com_issue_error(hr);
теперь запихиваем наш recordset в отчет:
IApplicationPtr pApp;
IReportPtr pRep;
hr=CoCreateInstance(CLSID_Application, NULL, CLSCTX_INPROC_SERVER , IID_IApplication,
(void **) &pApp);
if(FAILED(hr)) _com_issue_error(hr);
pRep=pApp->OpenReport(_bstr_t("d:\\proj\\SampRep\\Report1.rpt"));
m_CRView1.SetReportSource(pRep);
IDatabasePtr pDatabase = 0;
IDatabaseTablesPtr pTables = 0;
IDatabaseTablePtr pTable = 0;
pRep->get_Database((IDatabase**) &pDatabase);
pDatabase->get_Tables((IDatabaseTables**) &pTables);
VARIANT var, var2;
VariantInit(&var);
VariantInit(&var2);
var.vt = VT_DISPATCH;
var.pdispVal = (IDispatch*)pConn;
var2.vt = VT_DISPATCH;
var2.pdispVal = (IDispatch*)pRs->GetActiveCommand();
hr = pDatabase->AddADOCommand(var, var2);
ASSERT(SUCCEEDED(hr));
собираем проект. Всё готово.
/ / ВОПРОС-ОТВЕТ/ / / / / / / / / / / / / / / / / /
Как сделать нестандартную кнопку на основе битмапа (без MFC, только
WinAPI)?
Кнопка не обязательно должна иметь стандартный внешний вид (хотя лично я не нахожу внешний вид стандартной кнопки скучным или "простецким"). Однако для многих разработчиков и пользователей кнопки, имеющие нестандартный вид, выглядят более привлекательными. Поэтому для придания некоего стиля интерфейсу собственных программ можно использовать кнопки, отображающие некий битмап (bitmap - растровое изображение).
Кроме эффектов изображения можно использовать еще и эффекты формы - к примеру, круглая или овальная кнопка также достаточно оригинальны внешне, - но данная статья не рассматривает технику создания кнопок, имеющих форму, отличную от прямоугольной.
Windows имеет встроенные механизмы и API, поддерживающие создание кнопок (а также и других контролов), имеющих нестандартный внешний вид. Способ отрисовки внешнего вида контрола зависит от его стиля. В данном случае, стиль, нужный нам - это BS_OWNERDRAW. Из его названия видно, что отрисовку вида кнопки выполняет код пользователя, помещенный в оконную (диалоговую) функцию окна-владельца контрола.
Рассмотрим основные этапы отрисовки контрола, имеющего стиль xx_OWNERDRAW.
Поскольку мы реализуем, хотя и самостоятельно отрисовываемую, но все же кнопку, то было бы неплохо, если бы она имела поведение обычной кнопки - края кнопки в нормальном состоянии должны имитировать выпуклый контрол, при нажатом состоянии - вдавленный, при установленном фокусе кнопка должна иметь на себе прямоугольник, выполненный пунктирной линией, и в неактивном состоянии кнопка должна резко отличаться по цвету (либо фона, либо надписи, либо и того, и другого).
Выполняя указанные требования, мы можем подготовить четыре битмапа, реализующие внешний вид каждого из состояний кнопки, и отрисовывать в нужный момент (вот где появляется необходимость знать текущее состояние кнопки) одно из них. В этом случае мы сами полностью контролируем внешний вид кнопки в каждом из состояний. Впечатление, которое вы произведете на пользователя, будет целиком зависеть от вашего вкуса и умения создавать растровые изображения.
Что касается кода, реализующего необходимую логику работы, то его реализация может быть следующей:
BOOL CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
static HBITMAP hBm[BM_COUNT] = {NULL, NULL, NULL, NULL};
. . .
case WM_DRAWITEM:
return DrawFreeStyleBtn( (LPDRAWITEMSTRUCT) lParam, hBm );
. . .
}
BOOL DrawFreeStyleBtn( LPDRAWITEMSTRUCT pis, HBITMAP* phBm )
{
if( IDC_BMPBTN == pis->CtlID )
{
HBITMAP hOld = NULL;
HBITMAP hbm = phBm[BM_UP];
switch(pis->itemAction)
{
case ODA_DRAWENTIRE:
case ODA_SELECT:
if( pis->itemState & ODS_DISABLED )
hbm = phBm[BM_DISABLE];
else
if( pis->itemState & ODS_SELECTED )
hbm = phBm[BM_DOWN];
break;
case ODA_FOCUS:
if( pis->hwndItem == GetFocus() )
hbm = phBm[BM_FOCUS];
break;
}
HDC hCompDC = CreateCompatibleDC( pis->hDC );
hOld = (HBITMAP) SelectObject( hCompDC, hbm );
BitBlt(
pis->hDC,
pis->rcItem.left,
pis->rcItem.top,
pis->rcItem.right - pis->rcItem.left,
pis->rcItem.bottom - pis->rcItem.top,
hCompDC, 0, 0, SRCCOPY );
SelectObject( pis->hDC, hOld );
DeleteDC( hCompDC );
return TRUE;
}
return FALSE;
}
Как видим, ничего сложного. Код распадается на две части: в первой на основе сведений о выполняемых действиях (itemAction) и текущем состоянии кнопки (itemState) производится выбор необходимого битмапа, во второй части происходит вывод выбранного битмапа в контекст кнопки. Код обрамляется проверкой на необходимый идентификатор контрола, поскольку в рабочей программе подобных контролов может быть несколько.
Внимательный читатель готов задать вопрос о том, что в самом начале упоминались не только механизмы (реализованные, как мы выяснили, через сообщения WM_MEASUREITEM и WM_DRAWITEM), но и API?
Действительно, имеется несколько функций, облегчающих придание стандартного вида OWNERDRAW-контролам. Разработчик готовит только основной битмап для кнопки, а для отрисовки границ и состояний кнопки (неактивное и в фокусе) пользуется функциями WinAPI - DrawEdge() (границы контрола - "выпуклый/вдавленный"), DrawState() (состояние "активный/неактивный") и DrawFocusRect() (состояние "в фокусе"). В таком случае вышеприведенный код примет вид:
BOOL CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
static HBITMAP hBm = NULL;
. . .
case WM_DRAWITEM:
return DrawClassicStyleBtn( (LPDRAWITEMSTRUCT) lParam, hBm );
. . .
}
void DrawClassicStyleBtn(LPDRAWITEMSTRUCT pis, HBITMAP hBm, int deflate = 4)
{
UINT uState = DSS_NORMAL;
UINT uEdge = EDGE_RAISED;
int x = 0, y = 0;
BOOL bFocus = FALSE;
RECT rFocus;
if(IDC_BMPBTN == pis->CtlID)
{
switch(pis->itemAction)
{
case ODA_DRAWENTIRE:
case ODA_SELECT:
if(pis->itemState & ODS_DISABLED)
{
uState = DSS_DISABLED;
}
else
if(pis->itemState & ODS_SELECTED)
{
x += 1; // сдвиг всего рисунка вправо-вниз подчеркивает
y += 1; // визуальный эффект нажатия кнопки
uEdge = EDGE_SUNKEN;
}
break;
case ODA_FOCUS:
if(pis->hwndItem == GetFocus())
{
memcpy(&rFocus, &pis->rcItem, sizeof(RECT));
rFocus.left += deflate;
rFocus.top += deflate;
rFocus.right -= deflate;
rFocus.bottom -= deflate;
bFocus = TRUE;
}
break;
}
DrawState(
pis->hDC, NULL, NULL, (LONG)hBm, 0,
x, y, 0, 0, DST_BITMAP | uState);
DrawEdge(pis->hDC, &pis->rcItem, uEdge, BF_RECT);
if(bFocus)
DrawFocusRect(pis->hDC, &rFocus);
}
}
Выигрыш подобного подхода состоит в меньшем использовании самостоятельно подготавливаемых ресурсов и меньшем их потреблении при работе программы. К недостаткам (и весьма заметным, на мой взгляд) можно отнести то, что происходит потеря контроля над внешним видом кнопки в различных ее состояниях. Впрочем, работа этих упомянутых функций ориентирована на поддержание стандартного внешнего вида контролов, поэтому и результат не очень выразителен. На мой взгляд, данная техника больше подходит к выполнению кнопок, имеющих в основном стандартный внешний вид, но снабженных небольшими изображениями по соседству с текстом кнопки.
Следует заметить, что при необходимости можно (а иногда и нужно) пользоваться комбинацией приведенных методик: предположим, использовать для отрисовки чертыре битмапа, но границу рисовать функцией DrawEdge().
При подготовке данного материала мною использован код, опубликованный в одном из сообщений эхоконференции SU.WIN32.PROG (FidoNet). Автор кода - Dmitry Timoshkov <dmitry@sloboda.ru> - вполне может и не узнать его, поскольку код был мною довольно сильно переработан и дополнен :-))).
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
На сегодня все. До встречи!
Алекс Jenter
jenter@rsdn.ru
Красноярск, 2001. Рассылка является частью проекта RSDN.