热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Qt学习笔记(5)绘图五子棋游戏

在上一篇博客CQt学习笔记(4)绘图中介绍了Qt中的绘图方法,基于上一篇的博客的知识,使用QPainter设计一个五子棋的棋

          在上一篇博客C++ Qt学习笔记(4)绘图中介绍了Qt中的绘图方法,基于上一篇的博客的知识,使用QPainter设计一个五子棋的棋盘,后续会完成五子棋的游戏设计。

 1. 棋盘的设计

首先需要绘制棋盘的界面,这里采用的方法是,首先需要设定棋盘的大小,定义BOARD_WIDTH,以及BOARD_HEIGHT表示棋盘的大小,CELL_SIZE表示棋盘中每个格子的大小,单位为像素。START_POS表示棋盘中第一个方格左上角的坐标。WIDGET_SIZE设定绘图设备的大小,也即Widget窗口的初始大小。

然后通过:

setFixedSize(WIDGET_SIZE);

设置绘图设备为固定大小。

ROW_NUM_START, COLUMN_NUM_START表示绘制放个上的数字的起始位置,具体的值可以根据绘制的效果进行调节。

BOARD_WIDTH = 15;             // 表示有十五个格子

BOARD_HEIGHT = 15;

CELL_SIZE = (25,25);

START_POS = (40, 40)

ROW_NUM_START = (15, 45)

COLUMN_NUM_START = (39, 25)

绘制棋盘的代码如下所示:

void Widget::paintEvent(QPaintEvent *event)
{QPainter painter(this); // this代表Widget,及绘图设备painter.setRenderHint(QPainter::Antialiasing); // 绘画普通图形启用反走样, 即开启抗锯齿painter.setRenderHint(QPainter::TextAntialiasing); // 绘画文字反走样, 即开启抗锯齿int width = this->width(); // 获取绘图区域的大小int height = this->height();painter.fillRect(this->rect(), Qt::gray); //填充背景颜色// 设置字体QFont font;font.setPointSize(10);font.setBold(true);painter.setFont(font);// 设置画笔QPen pen;pen.setWidth(2); // 设置线宽pen.setColor(Qt::black); // 设置颜色pen.setStyle(Qt::SolidLine); // 设置线型pen.setCapStyle(Qt::FlatCap);pen.setJoinStyle(Qt::BevelJoin);painter.setPen(pen);for(int i=0; i

在绘制完棋盘之后,需要在棋盘上绘制一个落子提示的图标,随着鼠标的移动而提示落子的位置,这里需要使用到鼠标事件,

MouseMoveEvent()来实时获取鼠标的移动位置:
获取坐标以及计算落子位置的方法如下:
1. 获取鼠标在绘图设备中的实际位置:
2. 计算当前位置在棋盘中的坐标,将棋盘的起点START_POS看为(0,0)点

3. 得到鼠标在棋盘中的坐标

4. 给坐标加上一个半个方格大小的偏置量,此时相当于提示符号的位置从方格左上角偏移到十字交叉的位置。

5.判断鼠标的坐标是否在合理的范围之内。

void Widget::mouseMoveEvent(QMouseEvent *event) // 鼠标事件
{QPoint pos &#61; event->pos() - START_POS; // 相对于棋盘起始位置的坐标QPoint temp_point &#61; pos &#43; QPoint(CELL_SIZE.width()/2, CELL_SIZE.height()/2); // 坐标int x &#61; temp_point.x();int y &#61; temp_point.y();// 检测此时坐标的范围是否合理// if(x <&#61; START_POS.x() || y <&#61; START_POS.y() || x >&#61; BOARD_WIDTH*CELL_SIZE.width()-START_POS.x() || y >&#61; BOARD_HEIGHT*CELL_SIZE.height() - START_POS.y())if(x <&#61; 0 || y <&#61; 0 || x >&#61; BOARD_WIDTH*CELL_SIZE.width() || y >&#61; BOARD_HEIGHT*CELL_SIZE.height()){return;}int offset_x &#61; x % CELL_SIZE.width(); // 这个坐标表示不够一个cell_size的坐标的大小int offset_y &#61; y % CELL_SIZE.height();QPoint tip_position &#61; QPoint(x-offset_x, y-offset_y)&#43;START_POS-QPoint(CELL_SIZE.width()/2, CELL_SIZE.height()/2)&#43;QPoint(8, 8);setTrackPos(tip_position);
}

定义一个点trackPos来保存鼠标实时变化的坐标,在构造函数中&#xff0c;需要设置开启MouseTracking:

// 构造函数
Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget), trackPos(28, 28)
{setFixedSize(WIDGET_SIZE); // 设置窗口为固定大小setMouseTracking(true); // 开启MouseTrackingui->setupUi(this);
}

setTrackPos()方法用于更新窗口部件&#xff1a;

void Widget::setTrackPos(QPoint point)
{trackPos &#61; point;update(); // update的作用是更新窗口部件
}

当窗口部件更新后&#xff0c;就会重新绘制棋盘。

绘制落子提示位置的图标&#xff0c;在上述的MouseMoveEvent中已经获取到了绘制的坐标位置&#xff0c;接下来需要定义几个点&#xff0c;连接成图标&#xff1a;

painter.setPen(Qt::red);QPoint poses[12] &#61; {trackPos &#43; QPoint(0, 8),trackPos,trackPos &#43; QPoint(8, 0),trackPos &#43; QPoint(17, 0),trackPos &#43; QPoint(25, 0),trackPos &#43; QPoint(25, 8),trackPos &#43; QPoint(25, 17),trackPos &#43; QPoint(25, 25),trackPos &#43; QPoint(17, 25),trackPos &#43; QPoint(8, 25),trackPos &#43; QPoint(0, 25),trackPos &#43; QPoint(0, 17)};painter.drawPolyline(poses, 3); // poses相当于指针painter.drawPolyline(poses&#43;3, 3); // 从poses&#43;3的点开始&#xff0c;将三个点连成一条线painter.drawPolyline(poses&#43;6, 3);painter.drawPolyline(poses&#43;9, 3);

绘制后的棋盘效果如下所示&#xff1a;

其中红色部分表示落子的提示标记&#xff0c;会随着鼠标的移动而变动位置。

单机鼠标下棋&#xff1a;

单击鼠标下棋的操作需要使用鼠标事件完成&#xff0c;MouseReleasedEvent(),在鼠标送开始&#xff0c;将棋子放到棋盘相应的位置&#xff1a;

首先&#xff0c;用一个二维的数组表示棋盘相应位置的落子情况&#xff0c;board[ i][ j]

1表示白色玩家

2表示黑色玩家

首先在开始棋局之前&#xff0c;需要随棋盘进行初始化操作&#xff0c;初始化的流程如下所示&#xff1a;

需要定义一些棋局中常用的状态量&#xff1a;

// 定义变量QPoint trackPos; // 记录鼠标的位置bool endGame; // 标志游戏是否结束bool WHITE_PALYER; // 白色玩家bool BLACK_PLAYER;bool next_player; // 下一位玩家static const int NO_PIECE &#61; 0; // 用于标记棋盘中某个位置没有棋子// 定义玩家static const int WHITE_PIECE &#61; 1; // 白棋static const int BLACK_PIECE &#61; 2; // 黑棋

初始化的过程如下所示&#xff0c;这里添加了随即决定开局的是白棋还是黑棋的部分&#xff1a;

void Widget::initBoard()
{// 对棋盘进行初始化for(int i&#61;0; i u(0, 1);e.seed(10); // 设置随机数种子double rand_number &#61; e(); // 生成随机数if(rand_number > 0.5){// 白棋先落子WHITE_PALYER &#61; true;BLACK_PLAYER &#61; false;next_player &#61; WHITE_PALYER;}else{// 黑棋先落子WHITE_PALYER &#61; false;BLACK_PLAYER &#61; true;next_player &#61; BLACK_PLAYER;}}

在游戏的过程中&#xff0c;每次都是通过鼠标点击棋盘的某一位置&#xff0c;实现棋子的落下&#xff0c;所以需要是实现鼠标事件MouseReleasedEvent();

在MouseReleasedEvent()事件函数中&#xff0c;首先获取到鼠标按下时的坐标&#xff0c;其次&#xff0c;就是在鼠标所按下的坐标处绘制相应的棋子&#xff0c;MouseReleasedEvent()的实现如下所示&#xff1a;

void Widget::mouseReleaseEvent(QMouseEvent *event)
{if(!endGame) // 游戏未结束{QPoint pos &#61; event->pos() - START_POS;int x &#61; pos.x();int y &#61; pos.y();int x_pos &#61; x / CELL_SIZE.width(); // 整数&#xff0c;确定第几个格子int y_pos &#61; y / CELL_SIZE.height();int x_offset &#61; x % CELL_SIZE.width(); // 余数&#xff0c;计算是否需要偏移int y_offset &#61; y % CELL_SIZE.height();if(x_offset > CELL_SIZE.width()/2){x_pos&#43;&#43;;}if(y_offset > CELL_SIZE.height()/2){y_pos&#43;&#43;;}dropPiece(x_pos, y_pos); //落子}}

这里的dropPiece就是表示在对应的位置(x,y)处绘制棋子&#xff0c;具体的实现过程为&#xff1a;

void Widget::dropPiece(int x, int y)
{if(x>&#61;0 && x<&#61;BOARD_WIDTH && y>&#61;0 && y<&#61;BOARD_HEIGHT && board[x][y]&#61;&#61;NO_PIECE){if(next_player &#61;&#61; WHITE_PALYER){board[x][y] &#61; WHITE_PIECE; // 当前位置是白棋}else{board[x][y] &#61; BLACK_PIECE; // 当前位置是黑棋}// 切换落子的玩家next_player &#61; !next_player;// 判断输赢checkWinner();update(); // 更新窗口组件}}

在落子之后&#xff0c;还需要判断此时的输赢情况&#xff0c;这里使用checkWinner()实现&#xff1a;

bool Widget::isHFivePiece(int x, int y)
{// 判断水平方向int piece &#61; board[x][y]; // 当前棋子的值for(int i&#61;1; i<5; i&#43;&#43;){if(x&#43;i>BOARD_WIDTH || board[x&#43;i][y]!&#61;piece){return false;}}return true;
}bool Widget::isVFivePiece(int x, int y)
{// 判断垂直方向int piece &#61; board[x][y];for(int i&#61;1; i<5; i&#43;&#43;){if(y&#43;i>BOARD_HEIGHT || board[x][y&#43;i]!&#61;piece){return false;}}return true;
}bool Widget::isLeftSlash(int x, int y)
{// 沿着左对角线int piece &#61; board[x][y];for(int i&#61;1; i<5; i&#43;&#43;){if(x&#43;i>BOARD_WIDTH || y&#43;i>BOARD_HEIGHT || board[x&#43;i][y&#43;i]!&#61;piece){return false;}}return true;
}bool Widget::isRightSlash(int x, int y)
{// 沿着右对角线int piece &#61; board[x][y];for(int i&#61;1; i<5; i&#43;&#43;){if(x-i<0 || y&#43;i>BOARD_HEIGHT || board[x-i][y&#43;i]!&#61;piece){return false;}}return true;
}bool Widget::isFivePiece(int x, int y)
{// 是否赢棋return isHFivePiece(x, y) || isVFivePiece(x, y) || isLeftSlash(x, y) || isRightSlash(x, y);
}void Widget::checkWinner()
{bool fullPieces &#61; true; // 和棋for(int i&#61;0; i}

最后&#xff0c;在执行update()之后&#xff0c;需要在QPainterEvent()中再次绘制新添加的棋子&#xff0c;实际上是将棋盘重新刷新依次&#xff0c;添加新的棋子&#xff1a;
 

// 绘制棋子painter.setPen(Qt::NoPen);// 查看棋盘的状态for(int i&#61;0; i

最终的效果如下图所示&#xff1a;

1. 现在需要对代码进行一下整合&#xff0c;第一步是实现五子棋界面的部分&#xff0c;创建一个基于QWidget类的类BoardWidget来表示棋盘&#xff0c;以及在下棋过程中的具体操作&#xff0c;这里不需要创建.ui文件&#xff1a;

boardwidget.h文件的实现&#xff1a;包含棋盘基本功能的实现&#xff0c;如棋盘绘制&#xff0c;落子位置跟踪&#xff0c;鼠标事件实现落子&#xff0c;判断棋局输赢等功能&#xff0c;都在BoardWidget类中实现&#xff1a;

#ifndef BOARDWIDGET_H
#define BOARDWIDGET_H#include class BoardWidget : public QWidget
{Q_OBJECT
public:explicit BoardWidget(QWidget *parent &#61; 0);~BoardWidget();signals:void game_over(int winner); // 游戏结束的信号public slots:protected:void paintEvent(QPaintEvent* event);void mouseMoveEvent(QMouseEvent* event);void mouseReleaseEvent(QMouseEvent* event);public:void initBoard();
private:void setTrackPos(QPoint point); // 追踪鼠标位置void dropPiece(int x, int y); // 落子// 判断输赢bool isFivePiece(int x, int y);bool isVFivePiece(int x, int y); // 从(x,y)开始&#xff0c;沿着垂直方向bool isHFivePiece(int x, int y); // 从(x,y)开始&#xff0c;沿着水平方向bool isLeftSlash(int x, int y); // 从(x,y)开始&#xff0c;沿着左对角线bool isRightSlash(int x, int y); // 从(x,y)开始&#xff0c;沿着右对角线void checkWinner();// 定义变量QPoint trackPos; // 记录鼠标的位置bool endGame; // 标志游戏是否结束bool WHITE_PALYER; // 白色玩家bool BLACK_PLAYER;bool next_player; // 下一位玩家public:// 棋盘的大小15x15static const int BOARD_WIDTH &#61; 15;static const int BOARD_HEIGHT &#61; 15;int board[BOARD_WIDTH][BOARD_HEIGHT]; // 定义棋盘//棋盘起始的的位置 行和列static const QPoint ROW_NUM_START;static const QPoint COLUMN_NUM_START;// sizestatic const QSize WIDGET_SIZE;static const QSize CELL_SIZE;static const QPoint START_POS;static const int NO_PIECE &#61; 0; // 用于标记棋盘中某个位置没有棋子// 定义玩家static const int WHITE_PIECE &#61; 1; // 白棋static const int BLACK_PIECE &#61; 2; // 黑棋};#endif // BOARDWIDGET_H

boardwidget.cpp

#include "boardwidget.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include // 初始化参数
// 1. 棋盘的起始位置&#xff1a;
const QPoint BoardWidget::ROW_NUM_START(15, 45);
const QPoint BoardWidget::COLUMN_NUM_START(35, 25);// 2.size初始化
const QSize BoardWidget::WIDGET_SIZE(830, 830);
const QSize BoardWidget::CELL_SIZE(45, 45);const QPoint BoardWidget::START_POS(40, 40);BoardWidget::BoardWidget(QWidget *parent) : QWidget(parent), trackPos(28, 28)
{setFixedSize(WIDGET_SIZE); // 设置窗口为固定大小setMouseTracking(true); // 开启MouseTrackinginitBoard(); // 初始化棋盘
}BoardWidget::~BoardWidget()
{}void BoardWidget::paintEvent(QPaintEvent *event)
{QPainter painter(this); // this代表Widget&#xff0c;及绘图设备painter.setRenderHint(QPainter::Antialiasing); // 绘画普通图形启用反走样, 即开启抗锯齿painter.setRenderHint(QPainter::TextAntialiasing); // 绘画文字反走样&#xff0c; 即开启抗锯齿int width &#61; this->width(); // 获取绘图区域的大小int height &#61; this->height();painter.fillRect(this->rect(), Qt::gray); //填充背景颜色// 设置字体QFont font;font.setPointSize(10);font.setBold(true);painter.setFont(font);// 设置画笔QPen pen;pen.setWidth(2); // 设置线宽pen.setColor(Qt::black); // 设置颜色pen.setStyle(Qt::SolidLine); // 设置线型pen.setCapStyle(Qt::FlatCap);pen.setJoinStyle(Qt::BevelJoin);painter.setPen(pen);for(int i&#61;0; i{QPoint pos &#61; event->pos() - START_POS; // 相对于棋盘起始位置的坐标QPoint temp_point &#61; pos &#43; QPoint(CELL_SIZE.width()/2, CELL_SIZE.height()/2); // 坐标int x &#61; temp_point.x();int y &#61; temp_point.y();// 检测此时坐标的范围是否合理// if(x <&#61; START_POS.x() || y <&#61; START_POS.y() || x >&#61; BOARD_WIDTH*CELL_SIZE.width()-START_POS.x() || y >&#61; BOARD_HEIGHT*CELL_SIZE.height() - START_POS.y())if(x <&#61; 0 || y <&#61; 0 || x >&#61; BOARD_WIDTH*CELL_SIZE.width() || y >&#61; BOARD_HEIGHT*CELL_SIZE.height()){return;}int offset_x &#61; x % CELL_SIZE.width(); // 这个坐标表示不够一个cell_size的坐标的大小int offset_y &#61; y % CELL_SIZE.height();// 绘制的图标的位置&#xff0c;中心的为十字交叉的位置QPoint tip_position &#61; QPoint(x-offset_x, y-offset_y)&#43;START_POS-QPoint(CELL_SIZE.width()/2, CELL_SIZE.height()/2)&#43;QPoint(8, 8);setTrackPos(tip_position);}void BoardWidget::mouseReleaseEvent(QMouseEvent *event)
{if(!endGame) // 游戏未结束{QPoint pos &#61; event->pos() - START_POS;int x &#61; pos.x();int y &#61; pos.y();int x_pos &#61; x / CELL_SIZE.width(); // 整数&#xff0c;确定第几个格子int y_pos &#61; y / CELL_SIZE.height();int x_offset &#61; x % CELL_SIZE.width(); // 余数&#xff0c;计算是否需要偏移int y_offset &#61; y % CELL_SIZE.height();if(x_offset > CELL_SIZE.width()/2){x_pos&#43;&#43;;}if(y_offset > CELL_SIZE.height()/2){y_pos&#43;&#43;;}dropPiece(x_pos, y_pos); //落子}}void BoardWidget::initBoard()
{// 对棋盘进行初始化for(int i&#61;0; i u(0, 1);e.seed(10); // 设置随机数种子double rand_number &#61; e(); // 生成随机数if(rand_number > 0.5){// 白棋先落子WHITE_PALYER &#61; true;BLACK_PLAYER &#61; false;next_player &#61; WHITE_PALYER;}else{// 黑棋先落子WHITE_PALYER &#61; false;BLACK_PLAYER &#61; true;next_player &#61; BLACK_PLAYER;}update();}void BoardWidget::setTrackPos(QPoint point)
{trackPos &#61; point;update(); // update的作用是更新窗口部件
}void BoardWidget::dropPiece(int x, int y)
{if(x>&#61;0 && x<&#61;BOARD_WIDTH && y>&#61;0 && y<&#61;BOARD_HEIGHT && board[x][y]&#61;&#61;NO_PIECE){if(next_player &#61;&#61; WHITE_PALYER){board[x][y] &#61; WHITE_PIECE; // 当前位置是白棋}else{board[x][y] &#61; BLACK_PIECE; // 当前位置是黑棋}// 切换落子的玩家next_player &#61; !next_player;// 判断输赢checkWinner();update(); // 更新窗口组件}}bool BoardWidget::isHFivePiece(int x, int y)
{// 判断水平方向int piece &#61; board[x][y]; // 当前棋子的值for(int i&#61;1; i<5; i&#43;&#43;){if(x&#43;i>BOARD_WIDTH || board[x&#43;i][y]!&#61;piece){return false;}}return true;
}bool BoardWidget::isVFivePiece(int x, int y)
{// 判断垂直方向int piece &#61; board[x][y];for(int i&#61;1; i<5; i&#43;&#43;){if(y&#43;i>BOARD_HEIGHT || board[x][y&#43;i]!&#61;piece){return false;}}return true;
}bool BoardWidget::isLeftSlash(int x, int y)
{// 沿着左对角线int piece &#61; board[x][y];for(int i&#61;1; i<5; i&#43;&#43;){if(x&#43;i>BOARD_WIDTH || y&#43;i>BOARD_HEIGHT || board[x&#43;i][y&#43;i]!&#61;piece){return false;}}return true;
}bool BoardWidget::isRightSlash(int x, int y)
{// 沿着右对角线int piece &#61; board[x][y];for(int i&#61;1; i<5; i&#43;&#43;){if(x-i<0 || y&#43;i>BOARD_HEIGHT || board[x-i][y&#43;i]!&#61;piece){return false;}}return true;
}bool BoardWidget::isFivePiece(int x, int y)
{// 是否赢棋return isHFivePiece(x, y) || isVFivePiece(x, y) || isLeftSlash(x, y) || isRightSlash(x, y);
}void BoardWidget::checkWinner()
{bool fullPieces &#61; true; // 和棋for(int i&#61;0; i}

在设计完成BoardWidget类的设计后&#xff0c;需要设计一个游戏的界面&#xff0c;这个界面的作用是可以将棋盘&#xff0c;以及其他的一些功能性的按钮添加到上面&#xff0c;形成一个面向用户的游戏界面。将这个类命名为GameWidget类&#xff1a;

gamewidget.h文件&#xff1a;

#ifndef GAMEWIDGET_H
#define GAMEWIDGET_H#include
#include "boardwidget.h"class GameWidget : public QWidget
{Q_OBJECTpublic:GameWidget(QWidget *parent &#61; 0);~GameWidget();private:BoardWidget* board;private slots:void restart_game();public slots:void showWinner(int winner);};#endif // GAMEWIDGET_H

在gamewidget中&#xff0c;需要对棋盘以及按钮的位置进行布局&#xff0c;所以需要用到布局管理器&#xff0c;在界面中添加一个重新开始游戏的按钮&#xff0c;在这个按钮按下的时候&#xff0c;需要调用棋盘的初始化函数&#xff0c;将整个棋盘清空&#xff0c;同时&#xff0c;在判断为玩家赢棋之后&#xff0c;需要弹出一个模态对话框&#xff0c;进行提示&#xff1a;

#include "gamewidget.h"
#include
#include
#include
#include
#include
#include GameWidget::GameWidget(QWidget *parent): QWidget(parent)
{setWindowTitle("Gomoku game"); // 设置窗口标题&#xff1a;QVBoxLayout* mainLayout &#61; new QVBoxLayout(this); // 布局管理器 垂直board &#61; new BoardWidget(this); // 新建棋盘对象QHBoxLayout* horizon_layout &#61; new QHBoxLayout(this); // 水平布局管理器QPushButton* btn_new_game &#61; new QPushButton("Resart"); // 按钮&#xff0c;开始新游戏horizon_layout->addWidget(btn_new_game);horizon_layout->addStretch();mainLayout->addLayout(horizon_layout);mainLayout->addWidget(board);connect(btn_new_game, SIGNAL(clicked()), this, SLOT(restart_game()));// 将当前的按钮与当前页面的resart_game()槽函数连接&#xff0c;在槽函数中调用initBoard()connect(board, SIGNAL(game_over(int)), this, SLOT(showWinner(int)));// 这种连接方式才能保证信号与槽连接正确, 不同对象之间信号与槽连接传递消息
}GameWidget::~GameWidget()
{}void GameWidget::showWinner(int winner)
{QString playerName;if(winner&#61;&#61;BoardWidget::WHITE_PIECE){playerName &#61; "White Winner";// qDebug() <<"Winner is 1" <}void GameWidget::restart_game()
{qDebug() <<"Restart the game" <initBoard();
}

main.cpp文件&#xff1a;

#include "gamewidget.h"
#include int main(int argc, char *argv[])
{QApplication a(argc, argv);GameWidget w;w.show();return a.exec();
}

整个项目的目录如下所示&#xff1a;

需要在项目中的.pro文件中添加&#xff1a;

QMAKE_CXXFLAGS &#43;&#61; -std&#61;c&#43;&#43;11

使得Qt项目能够支持C&#43;&#43;11.

程序运行后的效果&#xff1a;

 

 

-----------------------------------------------------------------------------------------------------------------------------------------

由于刚开始学习Qt,对教材上好多的地方理解不到位&#xff0c;后续会不断完善五子棋程序。


推荐阅读
author-avatar
顽童0006_648
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有