Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

no pictures in helpfile



  • Hi,

    I'm currently trying to familiarize myself with the Qt help system. As I understand it, the help system is built on top of the qdoc system.

    So I have created some files for qdoc. The generation of the html files works and the files also look as expected.

    However, when I generate the help file and view it with QtAssistant, there is not a single image. I searched for additional information, but my uncle Ducky hardly finds any entries about qt-helpfiles.

    I vaguely remember reading somewhere that the images should be in the same directory as the html files, so I tried copying the images. Didn't make any difference, unfortunately.

    directory layout from QtCreator:

    Dirs.png

    My qdoc config file:

    project     = FalconView
    description = controlcenter for linuxcnc
    outputdir   = html
    headerdirs  = src
    sourcedirs  = src
    imagedirs   = src/images
    exampledirs = .
    version     = 0.1
    headers.fileextensions  = "*.h *.hpp"
    sources.fileextensions  = "*.cpp *.qdoc"
    qhp.FalconView.namespace     = de.schwarzrot.falconview.0.1
    qhp.FalconView.virtualFolder = FalconView
    qhp.FalconView.indexTitle    = User Guide
    

    the help project file:

    <?xml version="1.0" encoding="utf-8" ?>
    <QtHelpProject version="1.0">
        <namespace>de.schwarzrot.falconview.0.1</namespace>
        <virtualFolder>FalconView</virtualFolder>
        <filterSection>
            <filterAttribute>FalconView</filterAttribute>
            <filterAttribute>0.1</filterAttribute>
            <toc>
                <section title="User Guide" ref="index.html">
                    <section title="start the engine" ref="startup.html"/>
                    <section title="reference" ref="reference.html"/>
                    <section title="use case" ref="usage.html"/>
                </section>
            </toc>
            <keywords>
                <keyword name="fileManager" id="fileManager" ref="fileManager.html"/>
            </keywords>
            <files>
                <file>*.html</file>
            </files>
        </filterSection>
    </QtHelpProject>
    

    ... and here's how I build the help-file:

    #!/bin/bash
    project=FalconView
    
    qdoc ${project}.qdocconf
    cp ${project}.qhp html
    cd html
    cp -a images/* .
    qhelpgenerator ${project}.qhp
    mv ${project}.qch ..
    

    As mentioned - I tried with and without image copying.

    May be the biggest question for my understanding:
    Are the images put into the compressed helpfile, or do I have to provied them to the helpviewer, like any HTML-browser does?


  • Moderators

    qdoc is an internal tool of Qt, do not use it for other projects. Use Doxygen instead.



  • @sierdzio said in no pictures in helpfile:

    qdoc is an internal tool of Qt

    That's new to me, it is not mentioned in the documentation or did I miss it through all the years?



  • Hi,

    thank you for your attention!

    @sierdzio said in no pictures in helpfile:

    qdoc is an internal tool of Qt, do not use it for other projects.

    Hm, my expectations are not very high, so qdoc works fine for me and I have no issues.

    Opposed to the helpengine.
    I tried to follow the Qt example "contextsensitivehelp", but it does not work at all for me. Documentation says, that I should test helpfile-creation by loading it in QtAssistant.

    Well, QtAssistant shows the pages, but without pictures. But when I use helpengine like Qt examples, I have no documents, no keywords, nothing.
    Even if I use the same url, that QtAssistant claims to use, I get no document in my app. But setup returns ok.

    So I don't know what's wrong.
    I'll gonna try to build my own help engine.



  • @django-Reinhard said in no pictures in helpfile:

    I'll gonna try to build my own help engine.

    Done :)

    My uncle duck guided me to stackoverflow (last post), and with that info, it was straight forward. Qt already has all needed stuff - only had to assemble it new.

    According to my tests, I can answer my initial question:

    @django-Reinhard said in no pictures in helpfile:

    Are the images put into the compressed helpfile, or do I have to provied them to the helpviewer, like any HTML-browser does?

    Aperently not.

    Ok, what did I do?
    I have the qdoc-files in directory docs, which has a subdirectory images.
    qdoc then creates a directory 'html' with a subdirectory 'images' and copies all images to html/images.

    So I created a zip-file from the qdoc output - means all generated html-files and the image subdirectory.

    Base for help output is QTextBrowser. It already has complete functionality. Subclassing is anyway required to be able to override QTextBrowser::loadResource.

    So the minimal project with a working helpEngine looks like:

    MainWindow declaration:

    class MainWindow : public QMainWindow
    {
      Q_OBJECT
    public:
      explicit MainWindow(QWidget *parent = nullptr);
      virtual ~MainWindow();
    
      void createConnections();
      void loadPage(const QString& pgName);
      void loadPage(const QUrl& link);
    
    private:
      Ui::MainWindow* ui;
      HelpEngine*     he;
      QTextBrowser*   tb;
      };
    

    ... and MainWindow definition:

    MainWindow::MainWindow(QWidget *parent)
     : QMainWindow(parent)
     , ui(new Ui::MainWindow)
     , he(new HelpEngine(QApplication::applicationDirPath()
                       + "/../share/falconview/help/FalconView.qzh"
                       , this))
     , tb(new HTMLBrowser(*he, this)) {
      ui->setupUi(this);
      tb->setMinimumWidth(830);
    
      setCentralWidget(tb);
      createConnections();
      }
    
    
    MainWindow::~MainWindow() {
      }
    
    
    void MainWindow::createConnections() {
      connect(ui->actionExit,      &QAction::triggered, this, &QWidget::close);
      connect(ui->actionStartup,   &QAction::triggered, this, [=](){ loadPage("startup"); });
      connect(ui->actionReference, &QAction::triggered, this, [=](){ loadPage("reference"); });
      connect(ui->actionUsage,     &QAction::triggered, this, [=](){ loadPage("usage"); });
      }
    
    
    void MainWindow::loadPage(const QString &pgName) {
      qDebug() << "MW::loadPage(" << pgName << ")";
      tb->setSource(pgName + ".html");
      }
    
    
    void MainWindow::loadPage(const QUrl& link) {
      qDebug() << "MW::loadPage(" << link << ")";
      tb->setSource(link);
      }
    

    I renamed the zip-archive to *.qzh - shortcut for "Qt zipped helpfile" ;)

    For testing purpuse I added a menubar to mainwindow, and I used the menuentries to select pages from help-archive.

    HTMLBrowser is subclass of QTextBrowser:

    class HTMLBrowser : public QTextBrowser
    {
      Q_OBJECT
    public:
      explicit HTMLBrowser(HelpEngine& engine, QWidget* parent = nullptr);
      virtual ~HTMLBrowser();
    
      QVariant loadResource(int type, const QUrl &name);
    
    private:
      HelpEngine& engine;
      };
    

    Nothing special in implementation:

    HTMLBrowser::HTMLBrowser(HelpEngine& engine, QWidget* parent)
     : QTextBrowser(parent)
     , engine(engine) {
      }
    
    
    HTMLBrowser::~HTMLBrowser() {
      }
    
    
    QVariant HTMLBrowser::loadResource(int type, const QUrl& link) {
      qDebug() << "loadResource(" << type << "url:" << link;
    
      return engine.readFile(link.path());
      }
    

    ... and finally the "new" HelpEngine.
    First declaration:

    class HelpEngine : public QObject
    {
      Q_OBJECT
    public:
      explicit HelpEngine(const QString& helpFile, QObject *parent = nullptr);
    
      QVariant readFile(const QString& file);
      void     tellContent();
    
    protected:
      void     buildDir(const QVector<QZipReader::FileInfo> entries);
    
    private:
      QZipReader*        reader;
      QMap<QString, int> helpDir;
      };
    

    ... and here the implementation:

    HelpEngine::HelpEngine(const QString& helpFile, QObject *parent)
     : QObject(parent)
     , reader(new QZipReader(helpFile)) {
      buildDir(reader->fileInfoList());
      }
    
    
    void HelpEngine::buildDir(const QVector<QZipReader::FileInfo> entries) {
      int mx = entries.count();
    
      for (int i=0; i < mx; ++i) {
          if (!entries[i].isFile) continue;
          helpDir.insert(entries[i].filePath, i);
          }
      }
    
    
    QVariant HelpEngine::readFile(const QString& file) {
      if (!helpDir.contains(file)) return QVariant();
      QByteArray ba = reader->fileData(file);
    
      return QVariant(ba);
      }
    
    
    void HelpEngine::tellContent() {
      for (const QZipReader::FileInfo& e : reader->fileInfoList()) {
          qDebug() << "Helpfile-entry:" << e.filePath
                   << (e.isDir ? "Dir" : "")
                   << (e.isFile ? "File" : "")
                   << (e.isSymLink ? "SymLink" : "");
          }
      }
    

    works like charming :)

    ... and in my humble opinion its fast enuf to use it without sqlite.
    Well, I have to find a solution for keywords and index, but I'm confident, that is feasible.

    Cheers


  • Moderators

    @artwaw said in no pictures in helpfile:

    @sierdzio said in no pictures in helpfile:

    qdoc is an internal tool of Qt

    That's new to me, it is not mentioned in the documentation or did I miss it through all the years?

    It's not mentioned in the docs, but it has been said repeatedly on the mailing list. I have no idea why the docs have never been updated with this info.



  • @sierdzio Thank you!



  • Hi,

    help from compressed archive works no matter, whether the *.html-files where built by qdoc, doxygen or are handwritten.
    If firefox can display the page as local file, then it should be ready for help-browser as well.

    New changes for index and keyword support: I added *.qhp to archive. That file already contains all necessary informations. So I did it like QHelpEngine: create Standard-widgets, which can be included in some page layout.

    I created a help-"dialog" a dockable, which can be used floating or integrated into mainwindow.

    HelpDialog::HelpDialog(QWidget* parent)
     : QDockWidget(tr("Help"), parent)
     , he(new HelpEngine(Core().helpFilename(), this))
     , tb(new HTMLBrowser(*he))
     , cw(static_cast<HelpContentWidget*>(he->contentWidget()))
     , kw(static_cast<HelpKeywordWidget*>(he->keywordWidget())) {
      setObjectName("HelpDialog");
      tb->setMinimumWidth(830);
      setMinimumWidth(1100);
      QSplitter*  sh = new QSplitter(Qt::Horizontal, this);
      QTabWidget* tw = new QTabWidget(sh);
    
      sh->addWidget(tw);
      sh->addWidget(tb);
      tw->addTab(cw, tr("Content"));
      tw->addTab(kw, tr("Keywords"));
      setWidget(sh);
      connect(cw, &QTreeWidget::currentItemChanged, this, &HelpDialog::contentItemChanged);
      connect(kw, &QListWidget::currentItemChanged, this, &HelpDialog::keywordItemChanged);
      }
    
    
    void HelpDialog::contentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *) {
      const QString& page = current->data(1, Qt::DisplayRole).toString();
    
      tb->setSource(page);
      }
    
    
    void HelpDialog::keywordItemChanged(QListWidgetItem *current, QListWidgetItem *) {
      const QString& page = current->toolTip();
    
      tb->setSource(page);
      }
    

    FalconView_Help01.jpg

    Here the sources. First the content-widget, a subclass of QTreeWidget:

    class HelpContentWidget : public QTreeWidget
    {
      Q_OBJECT
    public:
      explicit HelpContentWidget(QWidget* parent = nullptr);
      virtual ~HelpContentWidget();
    
      void setFolderIcon(const QIcon& ico);
      void setHelpIcon(const QIcon& ico);
      void parse(const QByteArray& ba);
    
    protected:
      QTreeWidgetItem* createItem(const QDomElement& e, QTreeWidgetItem* parent);
      QTreeWidgetItem* processElement(const QDomElement& e, QTreeWidgetItem* parent);
      void processChildren(const QDomElement& e, QTreeWidgetItem* parentItem = nullptr);
      void processAttributes(const QDomElement& e, QTreeWidgetItem* item);
    
    private:
      int   level;
      QIcon folderIcon;
      QIcon helpIcon;
      };
    

    ... and its implementation:

    HelpContentWidget::HelpContentWidget(QWidget* parent)
     : QTreeWidget(parent) {
      header()->setStretchLastSection(true);
      setHeaderLabel(tr("Title"));
      }
    
    
    HelpContentWidget::~HelpContentWidget() {
      }
    
    
    void HelpContentWidget::parse(const QByteArray& ba) {
      QDomDocument doc;
    
      doc.setContent(ba);
      QDomNodeList links = doc.elementsByTagName("toc");
    
      level = 0;
      clear();
      for (int i=0; i < links.count(); ++i) {
          QDomNode link = links.item(i);
    
          qDebug() << "\n";
          qDebug() << "check entry #" << i;
          if (link.isElement()) {
             QDomElement e = link.toElement();
    
             processChildren(e);
             }
          }
      }
    
    
    void HelpContentWidget::processAttributes(const QDomElement& e, QTreeWidgetItem* item) {
      int mx = e.attributes().count();
    
      qDebug() << "processAttributes ...";
      if (mx > 0) {
         qDebug() << "element has" << mx << "attributes";
         for (int i=0; i < mx; ++i) {
             const QDomNode& n = e.attributes().item(i);
    
             qDebug() << "\tattribute:" << n.nodeName() << " => " << n.nodeValue();
             if (n.nodeName() == "title")    item->setText(0, n.nodeValue());
             else if (n.nodeName() == "ref") item->setText(1, n.nodeValue());
             }
         }
      else qDebug() << "element has NO attributes";
      }
    
    
    void HelpContentWidget::processChildren(const QDomElement& e, QTreeWidgetItem* parent) {
      qDebug() << "processChildren ... (level:" << level++ << ")";
    
      for (QDomNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) {
          if (n.isElement()) {
             qDebug() << "child is Element";
             processElement(n.toElement(), parent);
             }
          }
      --level;
      }
    
    
    QTreeWidgetItem* HelpContentWidget::createItem(const QDomElement &e, QTreeWidgetItem *parent) {
      QTreeWidgetItem* item;
    
      if (parent) item = new QTreeWidgetItem(parent);
      else        item = new QTreeWidgetItem(this);
      item->setIcon(0, folderIcon);
      processAttributes(e, item);
    
      return item;
      }
    
    
    QTreeWidgetItem* HelpContentWidget::processElement(const QDomElement& e, QTreeWidgetItem* parent) {
      qDebug() << "processElement - tag:" << e.tagName() << "text:" << e.text();
      QTreeWidgetItem* item = nullptr;
    
      if (e.tagName() == "section") {
         item = createItem(e, parent);
    
         processChildren(e, item);
         }
      return item;
      }
    
    
    void HelpContentWidget::setFolderIcon(const QIcon &ico) {
      folderIcon = ico;
      }
    
    
    void HelpContentWidget::setHelpIcon(const QIcon &ico) {
      helpIcon = ico;
      }
    

    ... followed by the keywordWidget:

    class HelpKeywordWidget : public QListWidget
    {
      Q_OBJECT
    public:
      explicit HelpKeywordWidget(QWidget* parent = nullptr);
      virtual ~HelpKeywordWidget();
    
      void parse(const QByteArray& ba);
      void setIcon(const QIcon& icon);
    
    protected:
      void processChildren(const QDomElement& e);
    
    private:
      QIcon icon;
      };
    

    with this implementation:

    HelpKeywordWidget::HelpKeywordWidget(QWidget* parent)
     : QListWidget(parent) {
      }
    
    
    HelpKeywordWidget::~HelpKeywordWidget() {
      }
    
    
    void HelpKeywordWidget::parse(const QByteArray &ba) {
      QDomDocument doc;
    
      doc.setContent(ba);
      QDomNodeList links = doc.elementsByTagName("keywords");
    
      clear();
      for (int i=0; i < links.count(); ++i) {
          QDomNode link = links.item(i);
    
          qDebug() << "\n";
          qDebug() << "check entry #" << i;
          if (link.isElement()) {
             QDomElement e = link.toElement();
    
             processChildren(e);
             }
          }
      }
    
    
    void HelpKeywordWidget::processChildren(const QDomElement& e) {
      qDebug() << "processChildren ... ";
    
      for (QDomNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) {
          if (n.isElement()) {
             qDebug() << "child is Element";
             QDomElement      elem = n.toElement();
             QListWidgetItem* item = new QListWidgetItem();
             int              mx   = elem.attributes().count();
    
             item->setIcon(icon);
             for (int i=0; i < mx; ++i) {
                 const QDomNode& n = elem.attributes().item(i);
    
                 if (n.nodeName() == "name")     item->setText(n.nodeValue());
                 else if (n.nodeName() == "ref") item->setToolTip(n.nodeValue());
                 }
             addItem(item);
             }
          else if (n.isEntity()) qDebug() << "child is Entity";
          else if (n.isAttr())   qDebug() << "child is Attribute";
          else if (n.isText())   qDebug() << "child is Text";
          }
      }
    
    
    void HelpKeywordWidget::setIcon(const QIcon &icon) {
      this->icon = icon;
      }
    

    Creation of helpfile is done with a little bash-script:

    #!/bin/bash
    project=FalconView
    
    qdoc ${project}.qdocconf
    cp ${project}.qhp html
    cd html
    zip -u9 ../FalconView.qzh *.html images/* FalconView.qhp
    

    I think that this solution can be used to create a helper solution for small applications with the least possible effort.


Log in to reply