讓 Qt 的 QTextEdit 可以貼圖片

| | 0 Comments| 09:04
Categories:

Qt 有提供一個內建 QTextEdit 元件(官方文件),可以用來顯示、編輯文件;而理論上,他也是有支援比較豐富的文字格式的;他基本上可以處理絕大部分的字型、排版等格式設定,同時也可以支援圖片的顯示。

不過由於他內部有自己的資源管理系統,再加上預設的設定問題,所以如果直接拿 QTextEdit 來用的話,其實「貼上圖片」這個動作應該算是不可用的。

根據官方文件的說法(頁面),他預設可以插入純文字、HTML、含有格式的文字;而如果要處理其他形式的資料,則會需要繼承 QTextEdit、然後重新實作 canInsertFromMimeData()insertFromMimeData() 這兩個函式,並針對 MIME 格式來做判斷。

在《Several ways of placing an image in a QTextEdit》這邊,有人有提供一個簡單的範例;不過這個例子並不能處理從 Word 複製來的資料,所以這邊又做了些修改如下:

class QImageTextEdit : public QTextEdit
{
public:
  QImageTextEdit(QWidget* pParent = nullptr) : QTextEdit(pParent) {}
 
  bool canInsertFromMimeData(const QMimeData* source) const
  {
    return source->hasImage() || source->hasUrls() 
           || QTextEdit::canInsertFromMimeData(source);
  }
 
  void insertFromMimeData(const QMimeData* source)
  {
    if (source->hasImage())
    {
      QImage img = qvariant_cast<QImage>(source->imageData());
      if (!img.isNull())
      {
        QUrl url(QString("dropped_image_%1").arg(++m_uCounter));
        document()->addResource(QTextDocument::ImageResource, url, img);
        textCursor().insertImage(url.toString());
      }
    }
    else if (source->hasUrls())
    {
      foreach(QUrl url, source->urls())
      {
        QString sName = url.toLocalFile();
        QImage img(sName);
        if (!img.isNull())
        {
          document()->addResource(QTextDocument::ImageResource, sName, img);
          textCursor().insertImage(sName);
        }
      }
    }
    else if (source->hasHtml())
    {
      QString sHtml = source->html().replace(R"(src="file://)", R"(src=")");
      textCursor().insertHtml(sHtml);
    }
    else
    {
      QTextEdit::insertFromMimeData(source);
    }
  }
 
protected:
  std::size_t  m_uCounter = 0;
};

首先,這邊的 canInsertFromMimeData() 主要是告知 Qt 系統,這個文字編輯器的元件可以支援包含圖片、或是包含網址的 MIME 資料,這樣在貼上這類型資料的時候,Qt 才會讓這個元件去處理。

insertFromMimeData() 裡面,則是真的去處理要貼上的 MIME 資料;這裡面分成三個部分、代表不同的來源;理論上這樣的寫法可以在一定的程度上處理貼上圖片、貼上圖片檔案、貼上包含圖片的 HTML 的情境(Word 複製的會是這種)。


內部的 QTextDocument 與影像資源

不過在開始講各別的內容之前,這邊要先提一下 QTextEdit 內部對於圖片到底是怎麼管理、使用的。

QTextEdit 的內部是使用 QTextDocument文件)來儲存整個要顯示的文件,而在文件內要顯示的圖檔,則是以「資源」(resource)的形式來做額外的管理、儲存;如果要在裡面加入一張圖片,基本上需要:

  • 透過 addResource() 這個函式、將 QImageQTextDocument::ImageResource 的形式加入 QTextDocument 內部的資源管理系統,之後則是要透過資源的名稱(URL 的形式)來存取。
  • 在加入資源後,就可以在內文中加入對應圖片的資源名稱(URL)、來顯示圖片。

下面就是一個簡單的例子:

QTextEdit* editor = new QImageTextEdit();

QImage image(R"(d:\image.jpg)");
editor->document()->addResource( QTextDocument::ImageResource,
  QUrl("mydata://image.jpg"), image);

editor->append("<img src=\"mydata://image.jpg\" />");

這邊就是先讀取 d:\image.jpg 這個檔案,然後以「mydata://image.jpg」這個名稱加入資源管理系統。

之後則是透過 QTextEditappend() 函式、以 HTML 的 <img> 標籤的形式附加在文件的最後。實際上,可以插入圖片名稱的方法很多種,這邊只是其中一種而已。

而如果有需要,則也可以透過 QTextDocumentresource() 這個函式來取得需要的影像資源。


針對圖片與檔案的處理

在對 QTextEdit 處理影像的方法有了基本的認知後,再回來看 insertFromMimeData() 的處理方法。

首先,第一段當 source->hasImage() 為真的時候,代表是從剪貼簿貼上純粹的圖片資料。這時候,可以透過 QMimeData官方文件)的 imageData() 這個函式,來取得影像資料;不過由於他回傳的形式是 QVariant、所以還需要自己轉型成 QImage 來用。

而在確認圖片有正確讀取後,則就可以透過 addResource() 這個函式來把剛讀進來的檔案加入 QTextDocument 的資源管理系統了~不過由於沒有對應可以當名稱的資訊,所以這邊是建立一個「dropped_image_0」這種類型的序列號字串、來做為名稱、以避免衝突。

之後,則是透過 textCursor() 這個函式、來取得對應目前文字輸入游標的位置的操作物件(文件)、然後透過他的 insertImage() 來在目前的位置插入這個圖片。


而如果是拖曳檔案到 QTextEdit 的時候,QMimeData 實際上會是包含一個或多個代表檔案的 URL,所以這時候 source->hasUrls() 會回傳 true,然後就需要針對 urls() 回傳的 URL 陣列個別作處理。

這邊個別處理的方法基本上和上面貼上圖片的時候一樣,唯一不同的,就是資源的名稱是直接拿檔案名稱來用了。


處理 HTML

上面兩個處理方法基本上都是 Stack Overflow 那篇文章提供的方案,這樣寫確實可以對應貼上單獨的圖片、或是從檔案總管拉圖片進來。

但是如果是想把 Word 這類的工具編輯好、含圖片的內容複製進來的話,那似乎就不能用了。

Heresy 試著從 Word 貼一段含圖片的內容道測試程式上,文字的部分算正常,圖片看來也有試著要處理、但是卻會因為讀不到圖片、而變成一個文件的 ICON…

稍微研究了一下,發現在要貼上從 Word 複製的內容的時候,雖然還是會經過 insertFromMimeData(),但是 QMimeDatahasImage()hasUrls() 都會回傳 false。如果是以《Several ways of placing an image in a QTextEdit》提供的版本的話,會變成是直接用 insertFromMimeData() 來插入內容。

而雖然看來內容有正確地插入、卻沒有辦法顯示圖片是什麼原因呢?測試了一下後,發現應該是這樣貼上來的資料會被視為是 HTML、而裡面的圖檔的標籤會是類似下面的形式:

<img width=196 height=209 
src=\"file://C:/Users/Heresy/AppData/Local/Temp/msohtmlclip1/01/clip_image002.png\"
alt=\"...  自動產生的描述\" v:shapes=\"圖片_x0020_1\">

這邊可以看到,他會將圖檔儲存在使用者的暫存資料夾裡面拿來使用。

如 Qt 這邊沒辦法正確讀取的原因,測試了一下後發現似乎是因為路徑前面多了「file://」、所以不是本地路徑的關係?

所以這邊臨時性的處理方法就很簡單了~就是上面的程式碼的第三段,也就是:

else if (source->hasHtml())
{
  QString sHtml = source->html().replace(R"(src="file://)", R"(src=")");
  textCursor().insertHtml(sHtml);
}

這邊基本上就是當來源是 HTML 類型的資料的時候,先透過 html() 這個函式來取得 HTML 格式的原始內容,然後很粗暴地、透過 QStringreplace() 把所有的「src="file://」取代成「src="」,讓他的圖檔 URL 變成 local 端的路徑了!

之後,再透過 QTextCursorinseertHtml() 把整個 HTML 插入到當下游標的位置,就可以了~這個狀況下,他會自己去分析 HTML、並且試著將裡面使用的圖檔讀取到資源管理器裡面、不需要手動處理。


透過這個方法,基本上算是勉強可以符合個人的需求,不過有的地方還是得繼續修改、追加。

比如說,目前這樣的寫法只是讓他可以當下在 QTextEdit 中可以顯示、並繼續編輯而已;如果想要儲存下來的話,雖然可以透過 toHtml()toMarkdown() 來輸出,但是真的要實用的話,有用到的圖片應該都還是得另外再處理、否則之後要讀取的時候應該會讀不到的(尤其是直接貼上的部分)。

而如果是要可以讀檔、繼續修改的話,採用計數器的形式來產生圖片的 URL 的形式其實也不算好了,如果還有需要讀取檔案的話,就有可能造成衝突;這部分應該還是得檢查是否有重複、或是改用 hash 之類的方法可能還比較合適?

這部分,就之後有機會再整理了。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *