前端使用 Konva 实现可视化设计器(7)- 导入导出、上一步、下一步

请大家动动小手,给我一个免费的 Star 吧~

这一章实现导入导出为JSON文件、另存为图片、上一步、下一步。

github源码

gitee源码

示例地址

在这里插入图片描述

导出为JSON文件

提取需要导出的内容

  getView() {
    // 复制画布
    const copy = this.render.stage.clone()
    // 提取 main layer 备用
    const main = copy.find('#main')[0] as Konva.Layer
    // 暂时清空所有 layer
    copy.removeChildren()

    // 提取节点
    let nodes = main.getChildren((node) => {
      return !this.render.ignore(node) && !this.render.ignoreDraw(node)
    })

    // 重新装载节点
    const layer = new Konva.Layer()
    layer.add(...nodes)
    nodes = layer.getChildren()

    // 计算节点占用的区域
    let minX = 0
    let maxX = copy.width()
    let minY = 0
    let maxY = copy.height()
    for (const node of nodes) {
      const x = node.x()
      const y = node.y()
      const width = node.width()
      const height = node.height()

      if (x < minX) {
        minX = x
      }
      if (x + width > maxX) {
        maxX = x + width
      }
      if (y < minY) {
        minY = y
      }
      if (y + height > maxY) {
        maxY = y + height
      }

      if (node.attrs.nodeMousedownPos) {
        // 修正正在选中的节点透明度
        node.setAttrs({
          opacity: copy.attrs.lastOpacity ?? 1
        })
      }
    }

    // 重新装载 layer
    copy.add(layer)

    // 节点占用的区域
    copy.setAttrs({
      x: -minX,
      y: -minY,
      scale: { x: 1, y: 1 },
      width: maxX - minX,
      height: maxY - minY
    })

    // 返回可视节点和 layer
    return copy
  }

1、首先复制一份画布
2、取出 main layer
3、筛选目标节点
4、计算目标节点占用区域
5、调整拷贝画布的位置和大小

导出 JSON

使用 stage 的 toJSON 即可。

  // 保存
  save() {
    const copy = this.getView()

    // 通过 stage api 导出 json
    return copy.toJSON()
  }

导入 JSON,恢复画布

相比导出,过程会比较复杂一些。

恢复节点结构

  // 恢复
  async restore(json: string, silent = false) {
    try {
      // 清空选择
      this.render.selectionTool.selectingClear()

      // 清空 main layer 节点
      this.render.layer.removeChildren()

      // 加载 json,提取节点
      const container = document.createElement('div')
      const stage = Konva.Node.create(json, container)
      const main = stage.getChildren()[0]
      const nodes = main.getChildren()

      // 恢复节点图片素材
      await this.restoreImage(nodes)

      // 往 main layer 插入新节点
      this.render.layer.add(...nodes)

      // 上一步、下一步 无需更新 history 记录
      if (!silent) {
        // 更新历史
        this.render.updateHistory()
      }
    } catch (e) {
      console.error(e)
    }
  }

1、清空选择
2、清空 main layer 节点
3、创建临时 stage
4、通过 Konva.Node.create 恢复 JSON 定义的节点结构
5、恢复图片素材(关键)

恢复图片素材

  // 加载 image(用于导入)
  loadImage(src: string) {
    return new Promise<HTMLImageElement | null>((resolve) => {
      const img = new Image()
      img.onload = () => {
        // 返回加载完成的图片 element
        resolve(img)
      }
      img.onerror = () => {
        resolve(null)
      }
      img.src = src
    })
  }
  // 恢复图片(用于导入)
  async restoreImage(nodes: Konva.Node[] = []) {
    for (const node of nodes) {
      if (node instanceof Konva.Group) {
        // 递归
        await this.restoreImage(node.getChildren())
      } else if (node instanceof Konva.Image) {
        // 处理图片
        if (node.attrs.svgXML) {
          // svg 素材
          const blob = new Blob([node.attrs.svgXML], { type: 'image/svg+xml' })
          // dataurl
          const url = URL.createObjectURL(blob)
          // 加载为图片 element
          const image = await this.loadImage(url)
          if (image) {
            // 设置图片
            node.image(image)
          }
        } else if (node.attrs.gif) {
          // gif 素材
          const imageNode = await this.render.assetTool.loadGif(node.attrs.gif)
          if (imageNode) {
            // 设置图片
            node.image(imageNode.image())
          }
        } else if (node.attrs.src) {
          // 其他图片素材
          const image = await this.loadImage(node.attrs.src)
          if (image) {
            // 设置图片
            node.image(image)
          }
        }
      }
    }
  }

关于恢复 svg,关键在于拖入 svg 的时候,记录了完整的 svg xml 在属性 svgXML 中。

关于恢复 gif、其他图片,拖入的时候记录其 src 地址,就可以恢复到节点中。

上一步、下一步

其实就是需要记录历史记录

历史记录

  history: string[] = []
  historyIndex = -1

  updateHistory() {
    this.history.splice(this.historyIndex + 1)
    this.history.push(this.importExportTool.save())
    this.historyIndex = this.history.length - 1
    // 历史变化事件
    this.config.on?.historyChange?.(_.clone(this.history), this.historyIndex)
  }

1、从当前历史位置,舍弃后面的记录
2、从当前历史位置,覆盖最新的 JSON 记录
3、更新位置
4、暴露事件(用于外部判断历史状态,以此禁用、启用上一步、下一步)

更新历史记录

一切会产生变动的位置都执行 updateHistory,如拖入素材、移动节点、改变节点位置、改变节点大小、复制粘贴节点、删除节点、改变节点的层次。具体代码就不贴了,只是在影响的地方执行一句:

this.render.updateHistory()

上一步、下一步方法

  prevHistory() {
    const record = this.history[this.historyIndex - 1]
    if (record) {
      this.importExportTool.restore(record, true)
      this.historyIndex--
      // 历史变化事件
      this.config.on?.historyChange?.(_.clone(this.history), this.historyIndex)
    }
  }

  nextHistory() {
    const record = this.history[this.historyIndex + 1]
    if (record) {
      this.importExportTool.restore(record, true)
      this.historyIndex++
      // 历史变化事件
      this.config.on?.historyChange?.(_.clone(this.history), this.historyIndex)
    }
  }

另存为图片

  // 获取图片
  getImage(pixelRatio = 1, bgColor?: string) {
    // 获取可视节点和 layer
    const copy = this.getView()

    // 背景层
    const bgLayer = new Konva.Layer()

    // 背景矩形
    const bg = new Konva.Rect({
      listening: false
    })
    bg.setAttrs({
      x: -copy.x(),
      y: -copy.y(),
      width: copy.width(),
      height: copy.height(),
      fill: bgColor
    })

    // 添加背景
    bgLayer.add(bg)

    // 插入背景
    const children = copy.getChildren()
    copy.removeChildren()
    copy.add(bgLayer)
    copy.add(children[0], ...children.slice(1))

    // 通过 stage api 导出图片
    return copy.toDataURL({ pixelRatio })
  }

主要关注有2点:
1、插入背景层
2、设置导出图片的尺寸

导出的时候,其实就是把当前矢量、非矢量素材统一输出为非矢量的图片,设置导出图片的尺寸越大,可以保留更多的矢量素材细节。

接下来,计划实现下面这些功能:

  • 实时预览窗
  • 对齐效果
  • 连接线
  • 等等。。。

是不是值得更多的 Star 呢?勾勾手指~

源码

gitee源码

示例地址

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
棋盘覆盖问题是经典的计算几何问题之一,用于解决如何用特殊形状的多个小块来覆盖一个大的矩形区域的问题。在Qt中,可以通过自定义QWidget并使用QPainter来实现棋盘覆盖问题的可,同时使用QStackedWidget来实现上一步下一步的功能。 具体实现步骤如下: 1. 定义一个自定义QWidget,用于绘制棋盘以及小块的形状,并重写paintEvent函数来实现绘制功能。 2. 在自定义QWidget中定义一个数据结构来存储棋盘和小块的状态,并实现一个函数来更新状态。 3. 在QWidget中添加两个按钮,一个用于上一步操作,一个用于下一步操作,并连接相应的槽函数。 4. 在主窗口中使用QStackedWidget,将自定义QWidget添加到其中,并设置初始状态。 5. 在主窗口中添加两个按钮,一个用于上一步操作,一个用于下一步操作,并连接相应的槽函数,用于实现上一步下一步的功能。 示例代码如下: ```cpp // 自定义QWidget class ChessBoardWidget : public QWidget { public: ChessBoardWidget(QWidget *parent = nullptr); ~ChessBoardWidget(); void updateStatus(); // 更新棋盘和小块状态 protected: void paintEvent(QPaintEvent *event) override; // 绘制函数 private: QVector<QVector<int>> m_boardStatus; // 棋盘状态 QVector<QVector<int>> m_blockStatus; // 小块状态 int m_boardSize; // 棋盘大小 int m_blockSize; // 小块大小 }; // 主窗口 class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); private slots: void onPrevButtonClicked(); // 上一步操作 void onNextButtonClicked(); // 下一步操作 private: QStackedWidget *m_stackedWidget; // QStackedWidget ChessBoardWidget *m_chessBoardWidget; // 自定义QWidget QPushButton *m_prevButton; // 上一步按钮 QPushButton *m_nextButton; // 下一步按钮 }; ``` 在ChessBoardWidget中实现绘制函数和更新状态函数: ```cpp void ChessBoardWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); // 绘制棋盘 painter.setPen(Qt::black); painter.setBrush(Qt::white); painter.drawRect(0, 0, m_boardSize, m_boardSize); // 绘制小块 QColor color[] = {Qt::red, Qt::green, Qt::blue, Qt::yellow}; for (int i = 0; i < m_boardStatus.size(); ++i) { for (int j = 0; j < m_boardStatus[i].size(); ++j) { if (m_blockStatus[i][j] != -1) { painter.setBrush(color[m_blockStatus[i][j]]); painter.drawRect(j * m_blockSize, i * m_blockSize, m_blockSize, m_blockSize); } } } } void ChessBoardWidget::updateStatus() { // 更新棋盘和小块状态 // ... // 更新完状态后调用update()函数触发绘制函数 update(); } ``` 在MainWindow中实现上一步下一步操作的槽函数: ```cpp void MainWindow::onPrevButtonClicked() { // 切换到上一步状态 // ... } void MainWindow::onNextButtonClicked() { // 切换到下一步状态 // ... } ``` 在MainWindow的构造函数中初始QStackedWidget和ChessBoardWidget,并添加到QStackedWidget中: ```cpp MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // 初始QStackedWidget m_stackedWidget = new QStackedWidget(this); setCentralWidget(m_stackedWidget); // 初始ChessBoardWidget m_chessBoardWidget = new ChessBoardWidget(m_stackedWidget); m_stackedWidget->addWidget(m_chessBoardWidget); // 初始按钮 m_prevButton = new QPushButton("Prev", this); m_nextButton = new QPushButton("Next", this); connect(m_prevButton, &QPushButton::clicked, this, &MainWindow::onPrevButtonClicked); connect(m_nextButton, &QPushButton::clicked, this, &MainWindow::onNextButtonClicked); // 添加按钮 QHBoxLayout *buttonLayout = new QHBoxLayout; buttonLayout->addWidget(m_prevButton); buttonLayout->addWidget(m_nextButton); QWidget *buttonWidget = new QWidget(this); buttonWidget->setLayout(buttonLayout); setCentralWidget(buttonWidget); } ``` 最后在onPrevButtonClicked和onNextButtonClicked槽函数中实现上一步下一步操作的切换即可。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值