MagicTool为仿照某产品的实现而提供的功能, 目的是尽量减少使用者切换工具的操作, 能够利用鼠标以不同的操作手势来实现不同的行为.
规格:
按下Magic按钮, 进入Magic模式.
- 染色体修型/切割: 在背景上按下鼠标, 画线经过一个染色体, 并在背景上释放鼠标.
将经过的第一个染色体拆分, 并抛弃小于阈值的部分. 识别:
- flyovers.size>=3.
- 从染色体外起步
- 从染色体外结束
- 中间经过至少一个染色体
- 要切割的就是遇到的第一个染色体
- 染色体增补: 在一个染色体内部按下鼠标, 拖动鼠标到这个染色体外部区域, 描绘区域再回到这个染色体放开鼠标.
将鼠标围起来的染色体外部的区域叠加到染色体上面.
- 从染色体里面进入
- 进入其他的染色体或背景
- 从同一个染色体结束
- 连接染色体: 在染色体内部按下鼠标, 移动鼠标到相邻的另一个染色体
量两个染色体连接到一起.
- 从一个染色体内部开始
- 经过其他的染色体和空白
- 在另一个染色体内结束
- 分割染色体: 在一个染色体内部按下鼠标, 在这个染色体内部移动鼠标并画一个封闭区域, 然后松开鼠标.
- 从一个染色体内部开始,
- 在同一个染色体内部结束
- 有一个封闭曲线
- 新增染色体: 在背景上按下鼠标, 画一个封闭曲线, 然后松开鼠标. 中间不经过任何item.
- 被围起来的区域就是新增的曲线.
- 必须有且仅有一个封闭曲线—- 只识别一个, 如果多了, 结果不确保!
实现机理:
鼠标行为的跟踪
- 当鼠标被按下时, 检查是否在某个Item上面.
- 当鼠标移动时, 检测是否在Item上面, 并且这个Item和之前的Item是否一样
- 当鼠标松开时, 退出状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| void ExtractScene::_dealSplitMousePress(QGraphicsSceneMouseEvent *event) { if(_getMouseState() == EMouseState::Waiting && _impl->_splitToolType!=ScriptItem::Style::SelectMode ) { if(event->button() == Qt::LeftButton) { _setMouseState(EMouseState::Drawing); auto cur_pt = event->scenePos(); _impl->_magicFlyChroms.clear(); auto item = _firstChromItemAt(cur_pt); _impl->_magicFlyChroms.push_back(item); ... } } ... }
void ExtractScene::_dealSplitMouseMove(QGraphicsSceneMouseEvent *event) { if(_getMouseState() == EMouseState::Drawing) { if(event->buttons() & Qt::LeftButton) { auto cur_pt = event->scenePos(); auto item = _firstChromItemAt(cur_pt); if(item != _impl->_magicFlyChroms.back()) { _impl->_magicFlyChroms.push_back(item); } ... } } ... }
void ExtractScene::_dealSplitMouseRelease(QGraphicsSceneMouseEvent *event) { if(_getMouseState() == EMouseState::Drawing) { if(event->button() == Qt::LeftButton) { auto cur_pt = event->scenePos(); auto item = _firstChromItemAt(cur_pt); if(item != _impl->_magicFlyChroms.back()) { _impl->_magicFlyChroms.push_back(item); } if(_impl->_curScript->valid()) { _applySplit(_impl->_curScript, _impl->_magicFlyChroms); } } ... } }
|
其中, _impl->_magicFlyChroms
是一个vector
, 它记录鼠标当前经过的ChromosomeContourItem
, 将Item的指针保存进去. 这里的一个小问题是, 我们每次都只检测topmost的那个item. 实际上, Qt的这个QGraphicsScene::itemsAt()
函数的行为有点奇特, 当两个Item叠加的时候, 随着鼠标的移动, 究竟哪个在最上面都不能确定, 因此, 虽然这里已经做了去重, 但是最终的结果仍然是可能存在重复的. 我们还要在后面必要的时候再做检查.
不过, 就一般而言, 这种实现已经是足够了的.
另外一种做法, 可以不跟踪鼠标的移动, 而是在用户释放鼠标之后, 统一做识别. 这需要利用到Qt的Path的intersects()
, 有时则需要用到OpenCV的相关函数. 这种做法的性能应该是更高的, 但是因为丢失了序列信息, 我们无法知道先进入的那个Item, 后进入的哪个Item, 后来证明并不能满足用户的诉求, 所以废弃了这种实现, 改为当前的设计.
手势操作的识别
当用户释放鼠标时, 识别用户的手势操作. 在_identifyMaginOp()
中实现. 它的两个参数, 第一个是用户鼠标移动形成的一条曲线, 第二个参数则是所有经过的ChromosomeContourItem
的指针. 后者经过了初步的去重操作, 但是并不一定保证.
1 2 3
| std::pair<ExtractScene::SplitOpType, std::vector<ChromosomeContourItem *> > ExtractScene::_identifyMaginOp( const ScriptItem *curve, const std::vector<ChromosomeContourItem *> &flyovers)
|
我们要根据flyovers
的首尾, 个数等信息来判断操作类型, 以及受影响的Item.
这里要注意的是连接操作. 连接操作因为包含了所有经过的item, 我们在mouseMove
的事件处理中是无法实现完全的去重的, 所以要在这里做进一步的去重. 而其他的操作因为只做一个, 则不需要.
1 2 3 4 5 6 7 8 9 10 11
| else if ( flyovers.size()>=2 && flyovers.front()!=nullptr && flyovers.back()!=nullptr && flyovers.front()!=flyovers.back()) { opType = SplitOpType::OpSticky; opItems = ranges::views::all(flyovers) | ranges::views::filter([](const auto p){return p!=nullptr;}) | ranges::to<std::vector>() | ranges::actions::sort | ranges::actions::unique ; }
|
这里我们使用rangeV3来实现这个过滤操作. 注意的是unique()
操作的微妙之处. 所谓的unique
, 不管是rangev3还是标准的std::unique
, 都是去掉相邻的重复值. 例如, 序列1,2,3,3,2
, 处理后的结果是1,2,3,2
, 而不是1,2,3
. 要得到真正的唯一的值序列, 需要首先做排序处理. 我们这里就是简单地对指针做了个排列. 其顺序并不重要, 重要的是通过排序, 将相同的item放到一起.
应用处理
在识别了手势之后, 剩下的事情就没有太多的复杂了. 在_applySplit()
函数中实现了这个处理. 简单来说, 就是要识别出新增的item, 要删除的item, 要修改的item, 分别发送给对应的undo command去处理.
例如, 下面的代码段是处理cut操作的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| if(optype==SplitOpType::OpCut) { auto contours = _analyzer->splitContour(_getWorkingMat(), opitems.front()->contour(), {curve_line, }, configer()->getMinChromosomeArea(), configer()->getMaxChromosomeArea() ); if(contours.empty()) { TRACE() << "错误: 切割不出染色体"; } else if(contours.size()==1) { chg_contour = contours.front(); chg_item = opitems.front(); } else { rmv_items.push_back(opitems.front()); new_contours.swap(contours); } }
|
在其中,
1 2 3 4
| std::vector<std::vector<cv::Point>> new_contours; std::vector<ChromosomeContourItem *> rmv_items; std::vector<cv::Point> chg_contour; ChromosomeContourItem * chg_item=nullptr;
|
最后, 我们统一处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| if(chg_item!=nullptr && !chg_contour.empty()) { auto new_chrom = _analyzer->extractChromoByContour(_getExtractSrcMat(), chg_contour, configer()->getFeatherSize()); ... _undoStack->push(new ReplaceChromCommand(this, chg_item->chromosome(), new_chrom)); }
else if(!rmv_items.empty() || !new_contours.empty()) { QList<Chromosome> rmvChromList; std::transform(rmv_items.cbegin(), rmv_items.cend(), std::back_inserter(rmvChromList), [](const ChromosomeContourItem* a){return a->chromosome(); }); QList<Chromosome> newChromList; ... _undoStack->push(new AddDelCommand(this, newChromList, rmvChromList, _getTaskStage(), _getMouseState(), _getTaskStage(), _getMouseState(), true)); }
|