/////////////////////////////////////////////////////////////////////////////// // Name: xrcshot.cpp // Purpose: Utility to take screenshots of the panels defined in XRC files // Author: Vadim Zeitlin // Created: 2009-02-12 // RCS-ID: $Id: minimal.cpp 57509 2008-12-23 00:35:18Z VZ $ // Copyright: (c) 2009 TT-Solutions SARL // Licence: wxWindows licence /////////////////////////////////////////////////////////////////////////////// // ============================================================================ // declarations // ============================================================================ // ---------------------------------------------------------------------------- // headers // ---------------------------------------------------------------------------- #include "wx/wxprec.h" #ifdef __BORLANDC__ #pragma hdrstop #endif #ifndef WX_PRECOMP #include "wx/app.h" #include "wx/frame.h" #include "wx/panel.h" #include "wx/image.h" #include "wx/dcmemory.h" #include "wx/dcscreen.h" #endif #include "wx/vector.h" #include "wx/cshelp.h" #include "wx/cmdline.h" #include "wx/filename.h" #include "wx/dcgraph.h" #include "wx/scopeguard.h" #include "wx/xrc/xmlres.h" #include "wx/html/htmprint.h" namespace XRCShot { // ---------------------------------------------------------------------------- // data structures // ---------------------------------------------------------------------------- // struct containing the name of the window, its level (0 for normal windows, 1 // for sub-items such as radio buttons in a radio box) and the associated help // string // // there is also an ordinal number implicitly associated with each such object // as they are stored in WindowInfos vector struct WindowInfo { // ctor for normal windows: takes bounding rectangle and sets indent to 0 WindowInfo(const wxString& name, const wxString& help, const wxRect& rect) : name(name), help(help), indent(0), rect(rect) {} // ctor for child items such as radio buttons: no bounding rectangle WindowInfo(const wxString& name, const wxString& help, int indent) : name(name), help(help), indent(indent) {} wxString name, help; int indent; wxRect rect; }; typedef wxVector WindowInfos; // ---------------------------------------------------------------------------- // helper functions // ---------------------------------------------------------------------------- // render the given window in the DC at the specified position void RenderWindow(wxDC& dc, wxTopLevelWindow *tlw, const wxPoint& pos = wxPoint(0, 0)); // render the HTML text in the DC at the specified position // // returns the vertical extent of rendered contents // // dc can be NULL in which case nothing is rendered and the function is only // called because of its return value int RenderHTML(wxDC *dc, int width, const wxString& html, const wxPoint& pos = wxPoint(0, 0)); // add all children of win which have the associated help string to // windowsWithHelp // // this function is meant to be called recursively void FindWindowsWithHelp(WindowInfos& windowsWithHelp, wxWindow *win); // create HTML annotation containing the help texts for all the windows wxString BuildHTMLAnnotation(const WindowInfos& windowsWithHelp); // annotate windows supposed to be drawn on the specified wxDC by // RenderWindow() with numbers referring to the items in the HTML text created // by BuildHTMLAnnotation() void AnnotateWindows(wxDC& dc, const WindowInfos& windowsWithHelp, const wxPoint& pos); // ---------------------------------------------------------------------------- // application class // ---------------------------------------------------------------------------- class App : public wxApp { public: App(); virtual void OnInitCmdLine(wxCmdLineParser& parser); virtual bool OnCmdLineParsed(wxCmdLineParser& parser); virtual bool OnInit(); private: // various command line options wxString m_inputFile, // name of input XRC file m_panelName, // name of the panel to load from it m_title; // title to use in the screenshot static const char *OPTION_TITLE; wxDECLARE_NO_COPY_CLASS(App); }; // ============================================================================ // implementation // ============================================================================ // ---------------------------------------------------------------------------- // CreateAnnotatedScreenshot() and its helpers // ---------------------------------------------------------------------------- // add all windows with non-empty help text to windowsWithHelp, recursively void FindWindowsWithHelp(WindowInfos& windowsWithHelp, wxWindow *win) { const wxString help = win->GetHelpText(); if ( !help.empty() ) { windowsWithHelp.push_back( WindowInfo(win->GetName(), help, win->GetRect())); } for ( wxWindowList::const_iterator it = win->GetChildren().begin(), end = win->GetChildren().end(); it != end; ++it ) { FindWindowsWithHelp(windowsWithHelp, *it); } // special radio box hack: we also want to include the help texts for the // individual radio buttons // // TODO: need to do it for the other wxControlWithItems? if ( wxRadioBox * const rbox = wxDynamicCast(win, wxRadioBox) ) { const unsigned count = rbox->GetCount(); for ( unsigned n = 0; n < count; ++n ) { const wxString itemHelp = rbox->GetItemHelpText(n); if ( !itemHelp.empty() ) { windowsWithHelp.push_back( WindowInfo(rbox->GetString(n), itemHelp, 1)); } } } } // escape the string for HTML wxString ToHTML(const wxString& text) { wxString html; html.reserve(text.length()); for ( wxString::const_iterator end = text.end(), it = text.begin(); it != end; ++it ) { switch ( const wchar_t ch = *it ) { case '<': html += "<"; break; case '>': html += ">"; break; case '&': html += "&"; break; default: html += ch; } } return html; } wxString BuildHTMLAnnotation(const WindowInfos& windowsWithHelp) { // TODO: make it possible to specify template for the HTML wxString html; html << "
    "; int currentIndent = 0; const size_t count = windowsWithHelp.size(); for ( size_t n = 0; n < count; ++n ) { const WindowInfo& wi = windowsWithHelp[n]; if ( wi.indent > currentIndent ) html << ""; currentIndent = wi.indent; html << "
  1. " << ToHTML(wi.name) << " : " << ToHTML(wi.help) << "
  2. "; } html << "
"; return html; } void AnnotateWindows(wxDC& dc, const WindowInfos& windowsWithHelp, const wxPoint& pos) { // set up the font and colours we use // // TODO: make configurable, check for contrast with theme colours? wxFont boldFont(dc.GetFont()); boldFont.SetWeight(wxFONTWEIGHT_BOLD); dc.SetFont(boldFont); dc.SetTextForeground(*wxWHITE); dc.SetBrush(*wxRED); dc.SetPen(*wxTRANSPARENT_PEN); // draw footnote-like circled numbers for all windows we annotate int itemNumber = 1; const size_t count = windowsWithHelp.size(); for ( size_t n = 0; n < count; ++n ) { const WindowInfo& wi = windowsWithHelp[n]; // we only annotate the top level children, annotating radio buttons // would make the bitmap too crowded if ( wi.indent ) continue; // notice that the item number is not the same as n as we only number // the top level children, here and in the HTML list const wxString label = wxString::Format("%d", itemNumber++); const wxSize sizeLabel = dc.GetTextExtent(label); // centre the footnote vertically but put it to the left of the window // and not in the centre as it doesn't overwrite the label of check or // radio boxes like this wxPoint posNote = pos + wi.rect.GetPosition(); posNote.x -= sizeLabel.x; posNote.y += (wi.rect.height + sizeLabel.y)/2; dc.DrawCircle(posNote, (3*wxMax(sizeLabel.x, sizeLabel.y))/4); posNote -= sizeLabel/2; dc.DrawText(label, posNote); } } wxBitmap CreateAnnotatedScreenshot(wxFrame *frame) { const wxSize sizeFrame = frame->GetSize(); // iterate over all the windows selecting the ones which have an associated // help string WindowInfos windowsWithHelp; FindWindowsWithHelp(windowsWithHelp, frame); // create the HTML annotation text const wxString html = BuildHTMLAnnotation(windowsWithHelp); // compute the size we need for it, in pixels, without really rendering it const int htmlHeight = RenderHTML(NULL, sizeFrame.x, html); // create the composite bitmap containing the screenshot of the window // itself and the HTML annotation wxBitmap bmp(sizeFrame.x, sizeFrame.y + htmlHeight); wxMemoryDC dc(bmp); dc.SetBackground(*wxWHITE); dc.Clear(); RenderWindow(dc, frame); RenderHTML(&dc, sizeFrame.x, html, wxPoint(0, sizeFrame.y)); // use wxGCDC if available for anti-aliased drawing of the annotations: // small circles look much better with anti-aliasing #if wxUSE_GRAPHICS_CONTEXT wxGCDC gc(dc); #else wxDC& gc = dc; #endif // notice that we need to account for the fact that positions of the // windows in windowsWithHelp are in client frame coordinates while we // render the full frame, including non-client parts, to the DC at (0, // 0) and hence the client area (0, 0) is offset from the origin of DC AnnotateWindows(gc, windowsWithHelp, (frame->GetSize() - frame->GetClientSize())/2 + frame->GetClientAreaOrigin()); return bmp; } // ---------------------------------------------------------------------------- // RenderXXX() functions // ---------------------------------------------------------------------------- void RenderWindow(wxDC& dc, wxTopLevelWindow *tlw, const wxPoint& pos) { // TODO: avoid showing it, e.g. by using WM_PRINT under MSW (and this // also seems not to work under Mac) tlw->Show(); tlw->Raise(); tlw->Update(); wxScreenDC dcScreen; dc.Blit(pos, tlw->GetSize(), &dcScreen, tlw->GetPosition()); } int RenderHTML(wxDC *dc, int width, const wxString& html, const wxPoint& pos) { wxHtmlDCRenderer rend; rend.SetStandardFonts(10); // TODO: make this configurable wxMemoryDC dummyDC; rend.SetDC(dc ? dc : &dummyDC); // we have no maximal height as we don't do pagination, we want to render // everything on a single bitmap -- but don't use INT_MAX as this results // in integer overflows in wxHTML code, INT_MAX/2 should be enough anyhow rend.SetSize(width, INT_MAX / 2); // call this after calling SetSize() to ensure it is laid out using the // width specified above rend.SetHtmlText(html); wxArrayInt pagebreaks; return rend.Render(pos.x, pos.y, pagebreaks, 0, dc == NULL); } // ---------------------------------------------------------------------------- // application class // ---------------------------------------------------------------------------- // names of the command-line options const char *App::OPTION_TITLE = "t"; App::App() { // we use non-default capitalization so prevent the default // GetAppDisplayName() implementation from mangling our name as "Xrcshot" SetAppDisplayName(_("XRCShot")); } void App::OnInitCmdLine(wxCmdLineParser& parser) { wxApp::OnInitCmdLine(parser); // TODO: add output file name (and format?) option parser.AddOption(OPTION_TITLE, "--title", _("title for the window in the screenshot")); parser.AddParam(_("XRC file name")); parser.AddParam(_("panel name in XRC"), wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL); } bool App::OnCmdLineParsed(wxCmdLineParser& parser) { if ( !wxApp::OnCmdLineParsed(parser) ) return false; m_inputFile = parser.GetParam(0); m_panelName = parser.GetParam(1); if ( !parser.Found(OPTION_TITLE, &m_title) ) m_title = wxString::Format(_("Screenshot of %s"), m_panelName); return true; } bool App::OnInit() { // load the specified XRC file if ( !wxApp::OnInit() ) return false; // we need to have a help provider for the help strings defined in the XRC // files to be used at all wxHelpProvider::Set(new wxSimpleHelpProvider); wxXmlResource * const xrc = wxXmlResource::Get(); if ( !xrc ) { wxLogError(_("Failed to initialize XRC resources.")); return false; } xrc->InitAllHandlers(); if ( !xrc->Load(m_inputFile) ) { wxLogError(_("Failed to load XRC file \"%s\"."), m_inputFile); return false; } // create a temporary frame to house the panel we're going to load wxFrame * const frame = new wxFrame(NULL, wxID_ANY, m_title); wxON_BLOCK_EXIT_OBJ0( *frame, wxFrame::Destroy ); wxPanel * const panel = xrc->LoadPanel(frame, m_panelName); if ( !panel ) { wxLogError(_("Failed to load panel \"%s\" from XRC file \"%s\"."), m_panelName, m_inputFile); return false; } frame->SetClientSize(panel->GetBestSize()); // create a bitmap containing the frame with annotations for all the // windows with help strings wxBitmap bmp(CreateAnnotatedScreenshot(frame)); // and save it wxImage::AddHandler(new wxPNGHandler); wxFileName fn(m_inputFile); fn.SetExt("png"); const wxString outFile = fn.GetFullName(); if ( !bmp.ConvertToImage().SaveFile(outFile, wxBITMAP_TYPE_PNG) ) { wxLogError(_("Failed to save bitmap to \"%s\"."), outFile); return false; } return true; } IMPLEMENT_APP(App) } // namespace XRCShot