查找选择
在QtWebEngine中,可以使用QWebEnginePage的findText方法来查找文本,查找成功以后,将自动选择当前文本。
QWebEnginePage可以通过QWebEngineView的page()来取得。
比如,如下代码可以在页面中查找hello,world并选择。
// view是一个QWebEngineView
auto page = view->page();
page->findText("hello,world");
findText方法的原型为:
void QWebEnginePage::findText(const QString &subString, QWebEnginePage::FindFlags options = {}, const std::function<void (const QWebEngineFindTextResult &)> &resultCallback = std::function<void(const QWebEngineFindTextResult &)>());
可以通过resultCallback这个参数,传递一个回调函数,根据QWebEngineFindTextResult变量,处理查找到的结果。
QWebEngineFindTextResult有两个方法,分别是activeMatch()和numberOfMatches(),分别用来表示当前激活的结果,以及一共查找到的结果总数。
获取选择文本
除了查找这种编程的方式以外,QWebEngineView作为一款浏览器控件,也支持用户手动选择。
当用户通过鼠标选择文本以后,可以通过QWebEnginePage的selectedText()方法来获得文本。
如:
auto text = mPage->selectedText ();
qDebug () << "user selected" << text;
获取选择位置
用户选择文本以后,我们除了想知道这段文本的内容以外,可能还需要知道这段文本的位置。即,这段文本在整个页面中处于什么位置。
QWebEngine并没有直接的方法,来取得一段文本的位置,但是QWebEnginePage有一个runJavaScript()方法,所以我们可以通过执行一些JavaScript,来间接地取得这些信息。
QWebEnginePage的runJavaScript()方法的原型为:
void QWebEnginePage::runJavaScript(const QString &scriptSource, const std::function<void (const QVariant &)> &resultCallback);
void QWebEnginePage::runJavaScript(const QString &scriptSource, quint32 worldId = 0, const std::function<void (const QVariant &)> &resultCallback = {})
即,我们可以通过回调函数,取得执行的JavaScript的结果。
基本的原理如下:
- 通过window.getSelection()取得所选区域Selection。
- 通过Selection的getRangeAt取得第一个Range。
- 分别返回Range的第一个节点的开头的全局偏移量,以及第二个节点的结尾的全局偏移量,为所选文本的偏移。
代码如下:
function getSelectionOffset() {
const selection = window.getSelection();
const createOffsetRange = (container, offset) => {
const range = document.createRange();
range.setStart(document.documentElement, 0);
range.setEnd(container, offset);
return range.toString().length;
};
try {
const range = selection.getRangeAt(0);
return [createOffsetRange(range.startContainer, range.startOffset), createOffsetRange(range.endContainer, range.endOffset)];
} catch (error) {
console.error('Error accessing selection range:', error);
return [null, null];
}
}
有了上面的JavaScript,我们就可以通过runJavaScript来获取结果了。
需要注意的是,runJavaScript是异步执行的。
即,如果我们需要在执行JavaScript结束之后 ,再接着执行runJavaScript后面的过程,需要手动加入同步代码。其中一个方法,是使用一个QEventLoop。
QEventLoop调用exec方法以后,遇到quit才会返回。
如:
int begin = -1;
int end = -1;
QEventLoop loop;
mPage->runJavaScript (
R"(
var selection = window.getSelection();
if (selection.rangeCount > 0) {
let range = selection.getRangeAt(0);
let start = document.createRange();
start.setStart(document.documentElement, 0);
start.setEnd(range.startContainer, range.startOffset);
let startOffset = start.toString().length;
let end = document.createRange();
end.setStart(document.documentElement, 0);
end.setEnd(range.endContainer, range.endOffset);
let endOffset = end.toString().length;
[startOffset, endOffset];
} else {
[null, null];
}
)",
[&loop, &begin, &end] (const QVariant &result) {
if (result.isValid () && result.typeId () == QMetaType::QVariantList)
{
auto offsets = result.toList ();
begin = offsets[0].toInt ();
end = offsets[1].toInt ();
qDebug () << "Begin offset:" << offsets[0].toInt();
qDebug () << "End offset:" << offsets[1].toInt();
loop.quit(); //结束QEventLoop
}
});
loop.exec (); //上面的loop.quit()之后,这里才返回。
return std::make_tuple (begin, end);
通过位置选择
能够通过选择取得位置,反过来就可以通过位置,进行选择。方法仍然是通过runJavaScript,这里不再示意runJavaScript的用法,只演示JavaScript代码。
选择的时候,需要根据上一步的全局偏移量,对整个页面的DOM进行遍历,找到相应的节点偏移量。
所以,这里分成三个函数实现:
选择一个节点的部分文本
选中的方法,是新建一个DocumentFragment,把不需要选择的文本,与选择的文本作为子节点加入,之后替换原来的节点为新建的DocumetFragment。
代码如下:
function underlineTextNode(textNode, startOffset, endOffset = -1) {
if (!(textNode instanceof Text)) {
throw new Error('Invalid text node provided');
}
const textContent = textNode.nodeValue;
const validEndOffset = endOffset === -1 ? textContent.length : endOffset;
if (startOffset < 0 || validEndOffset > textContent.length || startOffset > validEndOffset) {
throw new Error('Invalid offset values');
}
const parent = textNode.parentNode;
if (!parent) {
throw new Error('Text node has no parent element');
}
const beforeText = textContent.slice(0, startOffset);
const underlinedText = textContent.slice(startOffset, validEndOffset);
const afterText = textContent.slice(validEndOffset);
const underlineElement = document.createElement('u');
underlineElement.textContent = underlinedText;
const fragment = document.createDocumentFragment();
if (beforeText) fragment.appendChild(document.createTextNode(beforeText));
fragment.appendChild(underlineElement);
if (afterText) fragment.appendChild(document.createTextNode(afterText));
parent.replaceChild(fragment, textNode);
}
遍历函数,在回调中确定提前返回
function traverseTextNodes(root, callback) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null,);
let node;
while ((node = walker.nextNode())) {
if (callback(node) === false) break;
}
}
选择所有节点
方法是通过遍历,在回调函数中找到需要选择的所有节点。
然后,依次对每个节点调用第一个选择的函数。
function underlineByOffset(startOffset, endOffset) {
if (startOffset >= endOffset || startOffset < 0) {
throw new Error('Invalid offset range');
}
let currentOffset = 0;
const nodesInfo = {
start: {node: null, offset: 0}, end: {node: null, offset: 0}, between: []
};
traverseTextNodes(document.documentElement, (textNode) => {
const nodeLength = textNode.nodeValue.length;
const nodeEnd = currentOffset + nodeLength;
if (!nodesInfo.start.node && currentOffset <= startOffset && nodeEnd > startOffset) {
nodesInfo.start.node = textNode;
nodesInfo.start.offset = startOffset - currentOffset;
}
if (!nodesInfo.end.node && currentOffset <= endOffset && nodeEnd > endOffset) {
nodesInfo.end.node = textNode;
nodesInfo.end.offset = endOffset - currentOffset;
return false;
}
if (nodesInfo.start.node && !nodesInfo.end.node && textNode !== nodesInfo.start.node) {
nodesInfo.between.push(textNode);
}
currentOffset = nodeEnd;
return true;
});
if (nodesInfo.start.node && nodesInfo.end.node) {
underlineTextNode(nodesInfo.start.node, nodesInfo.start.offset, nodesInfo.start.node === nodesInfo.end.node ? nodesInfo.end.offset : -1);
nodesInfo.between.forEach(node => {
underlineTextNode(node, 0);
});
if (nodesInfo.start.node !== nodesInfo.end.node) {
underlineTextNode(nodesInfo.end.node, 0, nodesInfo.end.offset);
}
}
}