剖析QMenu & Qt完全定制化菜单
贴张效果图:
定制包括:
1. 周边阴影
2. 菜单项的元素(分割符, 控制ICON大小, 文字显示位置与颜色, 子菜单指示符)
菜单内的效果, 部分可以使用stylesheet实现, 但要做到这样的定制化, stylesheet是做不到的
下面介绍如何实现这些效果:
1. 实现阴影效果
默认的Qt菜单QMenu的效果是这样的
1) 首先需要去除下拉阴影(Drop shadow)
Qt的菜单是继承QWidget然后自绘的, dropshadow不属于自绘范围, 是windows popup类型窗口默认的样式, 无法通过正常途径去除
可以从源码中看到调用过程大概是这样:
qmenu::popup -> qwidget::show() -> QWidgetPrivate::show_helper() -> show_sys();
而这时候, 还未调用qmenu::paintevent
而且不能去除QMenu的Popup 属性, 因为QMenu的实现依赖Popup属性, 例如:
QMenuPrivate::activateAction中使用QApplication::activePopupWidget()函数
在windows平台下:
对窗口的handle操作, 可以去掉drop shadow. 参考http://stackoverflow.com/questions/13776119/qt-menu-without-shaodw
menu.h
#ifndef MENU_H
#define MENU_H #include <QMenu> class Menu : public QMenu
{
Q_OBJECT
public:
explicit Menu(QWidget *parent = 0);
explicit Menu(const QString & title); protected:
virtual bool event(QEvent *event); signals: public slots: }; #endif // MENU_H
menu.cpp
#include "menu.h" Menu::Menu(QWidget *parent) :
QMenu(parent)
{ } Menu::Menu(const QString &title) :
QMenu(title)
{ } bool Menu::event(QEvent *event)
{
static bool class_amended = false;
if (event->type() == QEvent::WinIdChange)
{
HWND hwnd = reinterpret_cast<HWND>(winId());
if (class_amended == false)
{
class_amended = true;
DWORD class_style = ::GetClassLong(hwnd, GCL_STYLE);
class_style &= ~CS_DROPSHADOW;
::SetClassLong(hwnd, GCL_STYLE, class_style);
} }
return QWidget::event(event);
}
大概思路是: 在event中截获QEvent::WinIdChange事件, 然后获得窗口handle, 使用GetClassLong
/ SetClassLong
去除 CS_DROPSHADOW
flags, 即可去除阴影
2) 使用dwm实现环绕阴影
优点:系统内置支持
缺点: 仅在vista以上并开启aero特效的情况, 使菜单有阴影环绕.
#pragma comment( lib, "dwmapi.lib" )
#include "dwmapi.h"
bool Menu::event(QEvent *event)
{
static bool class_amended = false;
if (event->type() == QEvent::WinIdChange)
{
HWND hwnd = reinterpret_cast<HWND>(winId());
if (class_amended == false)
{
class_amended = true;
DWORD class_style = ::GetClassLong(hwnd, GCL_STYLE);
class_style &= ~CS_DROPSHADOW;
::SetClassLong(hwnd, GCL_STYLE, class_style);
}
DWMNCRENDERINGPOLICY val = DWMNCRP_ENABLED;
::DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &val, sizeof(DWMNCRENDERINGPOLICY)); // This will turn OFF the shadow
// MARGINS m = {0};
// This will turn ON the shadow
MARGINS m = {-};
HRESULT hr = ::DwmExtendFrameIntoClientArea(hwnd, &m);
if( SUCCEEDED(hr) )
{
//do more things
}
}
return QWidget::event(event);
}
简单地修改一下event的实现即可
3) 手动绘制阴影
1. CCustomMenu 继承 QMenu
void CCustomMenu::_Init()
{
// 必须设置popup, 因为QMenuPrivate::activateAction中使用QApplication::activePopupWidget()函数
this->setWindowFlags(Qt::Popup | Qt::FramelessWindowHint);
this->setAttribute(Qt::WA_TranslucentBackground);
this->setObjectName("CustomMenu"); // 以objectname 区分Qt内置菜单和CCustomMenu }
设置菜单背景透明
objectname是为了在绘制时区分不同风格的菜单(比如原生Qmenu与CCustomMenu或者其他CCustomMenu2等)
2. 实现CCustomStyle (参考Qt的源码 QFusionStyle)
CCustomStyle继承自QProxyStyle, Qt控件中的基础元素都是通过style控制, style比stylesheet更底层, 可以做到更精细的控制
/**@brief 定制菜单style
@author lwh
*/
class CCustomStyle : public QProxyStyle
{
Q_OBJECT public:
CCustomStyle(QStyle *style = ); void drawControl(ControlElement control, const QStyleOption *option,
QPainter *painter, const QWidget *widget) const; void drawPrimitive(PrimitiveElement element, const QStyleOption *option,
QPainter *painter, const QWidget *widget) const; int pixelMetric ( PixelMetric pm, const QStyleOption * opt, const QWidget * widget) const; private:
void _DrawMenuItem(const QStyleOption *option,
QPainter *painter, const QWidget *widget) const;
QPixmap _pixShadow ; //阴影图片
};
首先需要调整菜单项与边框的距离, 用于绘制阴影
在pixelMetric 中添加
if(pm == PM_MenuPanelWidth)
return ; // 调整边框宽度, 以绘制阴影
pixelMetric 中描述了像素公制可取的一些值,一个像素公制值是单个像素在样式中表现的尺寸.
然后再drawPrimitive实现阴影绘制
void CCustomStyle::drawPrimitive( PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget ) const
{
if(element == PE_FrameMenu)
{
painter->save();
{
if(_pixShadow.isNull()
|| widget->objectName() != "CustomMenu") // fix bug: Qt的内置菜单显示不正常(如TextEdit右键菜单)
{
painter->restore();
return __super::drawPrimitive(element, option, painter, widget);
} QSize szThis = option->rect.size();
QPixmap pixShadowBg = _DrawNinePatch(szThis, _pixShadow);
painter->drawPixmap(option->rect, pixShadowBg);
}
painter->restore();
return;
}
__super::drawPrimitive(element, option, painter, widget);
}
QStyle::PE_FrameMenu Frame for popup windows/menus; see also QMenu.
注意: 绘制完直接return
_DrawNinePatch是以九宫格形式绘制, 将这样一张小的阴影图绘制到窗口时, 如果直接拉伸, 会变得非常模糊.
而九宫格形式可以绘制出相对漂亮的背景, 这种技巧同样可以应用在其他控件上.
const QPixmap _DrawNinePatch( QSize szDst, const QPixmap &srcPix )
{
// 绘制背景图到, 以九宫格形式 QPixmap dstPix(szDst);
dstPix.fill(QColor(, , , ));
QPainter painter;
painter.begin(&dstPix); int nW = szDst.width();
int nH = szDst.height(); int nWBg = srcPix.width();
int nHBg = srcPix.height();
QPoint m_ptBgLT(, );
QPoint m_ptBgRB(, ); QPoint ptDstLT(m_ptBgLT.x(), m_ptBgLT.y());
QPoint ptDstRB(nW-(nWBg-m_ptBgRB.x()), nH-(nHBg-m_ptBgRB.y())); //LT
painter.drawPixmap(QRect(,,ptDstLT.x(), ptDstLT.y()), srcPix, QRect(,,m_ptBgLT.x(), m_ptBgLT.y()));
//MT
painter.drawPixmap(QRect(ptDstLT.x(),, ptDstRB.x()-ptDstLT.x(), ptDstLT.y()), srcPix, QRect(m_ptBgLT.x(),,m_ptBgRB.x()-m_ptBgLT.x(), m_ptBgLT.y()));
//RT
painter.drawPixmap(QRect(ptDstRB.x(),,nW-ptDstRB.x(), ptDstLT.y()), srcPix, QRect(m_ptBgRB.x(),,nWBg-m_ptBgRB.x(), m_ptBgLT.y()));
//LM
painter.drawPixmap(QRect(,ptDstLT.y(),ptDstLT.x(), ptDstRB.y()-ptDstLT.y()), srcPix, QRect(,m_ptBgLT.y(),m_ptBgLT.x(), m_ptBgRB.y()-m_ptBgLT.y()));
//MM
painter.drawPixmap(QRect(ptDstLT.x(),ptDstLT.y(),ptDstRB.x()-ptDstLT.x(), ptDstRB.y()-ptDstLT.y()), srcPix, QRect(m_ptBgLT.x(),m_ptBgLT.y(),m_ptBgRB.x()-m_ptBgLT.x(), m_ptBgRB.y()-m_ptBgLT.y()));
//RM
painter.drawPixmap(QRect(ptDstRB.x(),ptDstLT.y(), nW-ptDstRB.x(), ptDstRB.y()-ptDstLT.y()), srcPix, QRect(m_ptBgRB.x(),m_ptBgLT.y(), nWBg-m_ptBgRB.x(), m_ptBgRB.y()-m_ptBgLT.y()));
//LB
painter.drawPixmap(QRect(,ptDstRB.y(),ptDstLT.x(), nH-ptDstRB.y()), srcPix, QRect(,m_ptBgRB.y(),m_ptBgLT.x(), nHBg-m_ptBgRB.y()));
//MB
painter.drawPixmap(QRect(ptDstLT.x(),ptDstRB.y(),ptDstRB.x()-ptDstLT.x(), nH-ptDstRB.y()), srcPix, QRect(m_ptBgLT.x(),m_ptBgRB.y(),m_ptBgRB.x()-m_ptBgLT.x(), nHBg-m_ptBgRB.y()));
//RB
painter.drawPixmap(QRect(ptDstRB.x(),ptDstRB.y(),nW-ptDstRB.x(), nH-ptDstRB.y()), srcPix, QRect(m_ptBgRB.x(),m_ptBgRB.y(),nWBg-m_ptBgRB.x(), nHBg-m_ptBgRB.y())); painter.end();
return dstPix;
}
2. 绘制菜单项
1) 控制ICON大小
在pixelMetric中:
if (pm == QStyle::PM_SmallIconSize)
return ; //返回ICON的大小
2) 绘制菜单项内容
void CCustomStyle::drawControl( ControlElement control, const QStyleOption *option, QPainter *painter, const QWidget *widget ) const
{
switch(control )
{
case CE_MenuItem:
{
_DrawMenuItem(option, painter, widget);
return; // 直接返回, 否则会被super::drawcontrol覆盖
}
}
__super::drawControl(control, option, painter, widget);
}
void CCustomStyle::_DrawMenuItem(const QStyleOption *option, QPainter *painter, const QWidget *widget ) const
{
painter->save(); if (const QStyleOptionMenuItem *menuItem = qstyleoption_cast<const QStyleOptionMenuItem *>(option))
{
// 先绘制一层背景(否则在透明情况下, 会直接透过去);
painter->setPen(colItemBg);
painter->setBrush(colItemBg);
painter->drawRect(option->rect); if (menuItem->menuItemType == QStyleOptionMenuItem::Separator) {
int w = ;
if (!menuItem->text.isEmpty()) { // 绘制分隔符文字
painter->setFont(menuItem->font);
proxy()->drawItemText(painter, menuItem->rect.adjusted(, , -, ), Qt::AlignLeft | Qt::AlignVCenter,
menuItem->palette, menuItem->state & State_Enabled, menuItem->text,
QPalette::Text);
w = menuItem->fontMetrics.width(menuItem->text) + ;
}
painter->setPen(colSeparator);
bool reverse = menuItem->direction == Qt::RightToLeft;
painter->drawLine(menuItem->rect.left() + + (reverse ? : w), menuItem->rect.center().y(),
menuItem->rect.right() - - (reverse ? w : ), menuItem->rect.center().y());
painter->restore();
return;
}
bool selected = menuItem->state & State_Selected && menuItem->state & State_Enabled;
if (selected) {
QRect r = option->rect;
painter->fillRect(r, colItemHighlight);
}
bool checkable = menuItem->checkType != QStyleOptionMenuItem::NotCheckable;
bool checked = menuItem->checked;
bool sunken = menuItem->state & State_Sunken;
bool enabled = menuItem->state & State_Enabled; bool ignoreCheckMark = false;
int checkcol = qMax(menuItem->maxIconWidth, ); if (qobject_cast<const QComboBox*>(widget))
ignoreCheckMark = true; //ignore the checkmarks provided by the QComboMenuDelegate if (!ignoreCheckMark) {
// Check
QRect checkRect(option->rect.left() + , option->rect.center().y() - , , );
checkRect = visualRect(menuItem->direction, menuItem->rect, checkRect);
if (checkable) {
if (menuItem->checkType & QStyleOptionMenuItem::Exclusive) {
// Radio button 未实现
if (checked || sunken) {
/* painter->setRenderHint(QPainter::Antialiasing);
painter->setPen(Qt::NoPen); QPalette::ColorRole textRole = !enabled ? QPalette::Text:
selected ? QPalette::HighlightedText : QPalette::ButtonText;
painter->setBrush(option->palette.brush( option->palette.currentColorGroup(), textRole));
painter->drawEllipse(checkRect.adjusted(4, 4, -4, -4));
*/
}
} else {
// Check box
if (menuItem->icon.isNull()) {
QStyleOptionButton box;
box.QStyleOption::operator=(*option);
box.rect = checkRect;
if (checked)
box.state |= State_On;
proxy()->drawPrimitive(PE_IndicatorCheckBox, &box, painter, widget); }
}
}
} else { //ignore checkmark
if (menuItem->icon.isNull())
checkcol = ;
else
checkcol = menuItem->maxIconWidth;
} // Text and icon, ripped from windows style
bool dis = !(menuItem->state & State_Enabled);
bool act = menuItem->state & State_Selected;
const QStyleOption *opt = option;
const QStyleOptionMenuItem *menuitem = menuItem; QPainter *p = painter;
QRect vCheckRect = visualRect(opt->direction, menuitem->rect,
QRect(menuitem->rect.x() + , menuitem->rect.y(),
checkcol, menuitem->rect.height()));
if (!menuItem->icon.isNull()) {
QIcon::Mode mode = dis ? QIcon::Disabled : QIcon::Normal;
if (act && !dis)
mode = QIcon::Active;
QPixmap pixmap; int smallIconSize = proxy()->pixelMetric(PM_SmallIconSize, option, widget);
QSize iconSize(smallIconSize, smallIconSize);
if (const QComboBox *combo = qobject_cast<const QComboBox*>(widget))
iconSize = combo->iconSize();
if (checked)
pixmap = menuItem->icon.pixmap(iconSize, mode, QIcon::On);
else
pixmap = menuItem->icon.pixmap(iconSize, mode); int pixw = pixmap.width();
int pixh = pixmap.height(); QRect pmr(, , pixw, pixh);
pmr.moveCenter(vCheckRect.center());
painter->setPen(colText);//menuItem->palette.text().color()
if (checkable && checked) {
QStyleOption opt = *option;
if (act) {
QColor activeColor = mergedColors(
colItemBg, //option->palette.background().color(),
colItemHighlight // option->palette.highlight().color());
);
opt.palette.setBrush(QPalette::Button, activeColor);
}
opt.state |= State_Sunken;
opt.rect = vCheckRect;
proxy()->drawPrimitive(PE_PanelButtonCommand, &opt, painter, widget);
}
painter->drawPixmap(pmr.topLeft(), pixmap);
}
if (selected) {
painter->setPen(colText);//menuItem->palette.highlightedText().color()
} else {
painter->setPen(colText); //menuItem->palette.text().color()
}
int x, y, w, h;
menuitem->rect.getRect(&x, &y, &w, &h);
int tab = menuitem->tabWidth;
QColor discol;
if (dis) {
discol = colDisText; //menuitem->palette.text().color()
p->setPen(discol);
}
int xm = windowsItemFrame + checkcol + windowsItemHMargin + ;
int xpos = menuitem->rect.x() + xm; QRect textRect(xpos, y + windowsItemVMargin, w - xm - windowsRightBorder - tab + , h - * windowsItemVMargin);
QRect vTextRect = visualRect(opt->direction, menuitem->rect, textRect);
QString s = menuitem->text;
if (!s.isEmpty()) { // draw text
p->save();
int t = s.indexOf(QLatin1Char('\t'));
int text_flags = Qt::AlignVCenter | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine;
if (!__super::styleHint(SH_UnderlineShortcut, menuitem, widget))
text_flags |= Qt::TextHideMnemonic;
text_flags |= Qt::AlignLeft;
if (t >= ) {
QRect vShortcutRect = visualRect(opt->direction, menuitem->rect,
QRect(textRect.topRight(), QPoint(menuitem->rect.right(), textRect.bottom())));
if (dis && !act && proxy()->styleHint(SH_EtchDisabledText, option, widget)) {
p->setPen(colText);//menuitem->palette.light().color()
p->drawText(vShortcutRect.adjusted(, , , ), text_flags, s.mid(t + ));
p->setPen(discol);
}
p->drawText(vShortcutRect, text_flags, s.mid(t + ));
s = s.left(t);
}
QFont font = menuitem->font;
// font may not have any "hard" flags set. We override
// the point size so that when it is resolved against the device, this font will win.
// This is mainly to handle cases where someone sets the font on the window
// and then the combo inherits it and passes it onward. At that point the resolve mask
// is very, very weak. This makes it stonger.
font.setPointSizeF(QFontInfo(menuItem->font).pointSizeF()); if (menuitem->menuItemType == QStyleOptionMenuItem::DefaultItem)
font.setBold(true); p->setFont(font);
if (dis && !act && proxy()->styleHint(SH_EtchDisabledText, option, widget)) {
p->setPen(menuitem->palette.light().color());
p->drawText(vTextRect.adjusted(, , , ), text_flags, s.left(t));
p->setPen(discol);
}
p->drawText(vTextRect, text_flags, s.left(t));
p->restore();
} // Arrow 绘制子菜单指示符
if (menuItem->menuItemType == QStyleOptionMenuItem::SubMenu) {// draw sub menu arrow
int dim = (menuItem->rect.height() - ) / ;
PrimitiveElement arrow;
arrow = option->direction == Qt::RightToLeft ? PE_IndicatorArrowLeft : PE_IndicatorArrowRight;
int xpos = menuItem->rect.left() + menuItem->rect.width() - - dim;
QRect vSubMenuRect = visualRect(option->direction, menuItem->rect,
QRect(xpos, menuItem->rect.top() + menuItem->rect.height() / - dim / , dim, dim));
QStyleOptionMenuItem newMI = *menuItem;
newMI.rect = vSubMenuRect;
newMI.state = !enabled ? State_None : State_Enabled;
if (selected)
newMI.palette.setColor(QPalette::ButtonText, // 此处futionstyle 有误, QPalette::Foreground改为ButtonText
colIndicatorArrow);//newMI.palette.highlightedText().color()
else
newMI.palette.setColor(QPalette::ButtonText,
colIndicatorArrow); proxy()->drawPrimitive(arrow, &newMI, painter, widget);
}
}
painter->restore();
}
_DrawMenuItem
_DrawMenuItem的代码较长, 但比较简单, 都是一些条件判断加上绘图语句, 需要自己修改pallete的颜色
值得注意的是: 在透明情况下, 应先绘制一层menu item 的背景, 否则会直接透过去
3) 最后还要重写一下QMenu的addMenu
以使子菜单也生效
QAction * CCustomMenu::addMenu( CCustomMenu *menu )
{
return QMenu::addMenu(menu);
} CCustomMenu * CCustomMenu::addMenu( const QString &title )
{
CCustomMenu *menu = new CCustomMenu(title, this);
addAction(menu->menuAction());
return menu;
} CCustomMenu * CCustomMenu::addMenu( const QIcon &icon, const QString &title )
{
CCustomMenu *menu = new CCustomMenu(title, this);
menu->setIcon(icon);
addAction(menu->menuAction());
return menu;
}
完整的工程代码在此, https://bitbucket.org/lingdhox/misc/src 或者CSDN http://download.csdn.net/detail/l470080245/6731989
编译需要VS2010+Qt5.
PS:
关于QMenu如何处理菜单消失, 参考我的另一篇blog Qt中QMenu的菜单关闭处理方法
剖析QMenu & Qt完全定制化菜单的更多相关文章
- Gradle 实现 Android 多渠道定制化打包
Gradle 实现 Android 多渠道定制化打包 版权声明:本文为博主原创文章,未经博主允许不得转载. 最近在项目中遇到需要实现 Apk 多渠道.定制化打包, Google .百度查找了一些资料, ...
- Oracle Sales Cloud:管理沙盒(定制化)小细节2——使用对象触发器更新数字字段
在上一篇 "管理沙盒(定制化)小细节1" 的随笔中,我们使用公式法在 "业务机会" 对象(单头)上建立了 "利润合计" 字段,并将它等于 & ...
- Oracle Sales Cloud:管理沙盒(定制化)小细节1——利用公式创建字段并显示在前端页面
Oracle Sales Cloud(Oracle 销售云)是一套基于Oracle云端的CRM管理系统.由于 Oracle 销售云是基于 Oracle 云环境的,它与传统的管理系统相比,显著特点之一便 ...
- 利用Qt Assistant 定制帮助文档
为了将Qt Assistant定制为自己应用程序的帮助文档浏览器.需要完成以下几步: 一.导入HTML格式的帮助文档 (1)首先,针对自己的应用程序创建HTML格式的帮助文档,请参见<Doxyg ...
- AI应用开发实战 - 定制化视觉服务的使用
AI应用开发实战 - 定制化视觉服务的使用 本篇教程的目标是学会使用定制化视觉服务,并能在UWP应用中集成定制化视觉服务模型. 前一篇:AI应用开发实战 - 手写识别应用入门 建议和反馈,请发送到 h ...
- Qt窗口定制
qt中的QWidget窗口支持窗体绘制,但是不支持窗口标题栏绘制,想要美观的界面,还需要自己去定制,下面我就介绍一种定制窗体的方法 一个窗口无非就3部分,标题栏.窗体和状态栏,接下来我定制的窗口没有状 ...
- Qt532.【转】Qt创建鼠标右键菜单
ZC:可以通过 设置 (QWebView*)->setContextMenuPolicy(NoContextMenu); 来关闭 QWebView的默认右键菜单 Qt创建鼠标右键菜单_疯华正茂 ...
- CentOS6.7定制化制作ISO
CentOS6.7定制化制作ISO 以CentOS 6.7-minimal为例. 欢迎大家转载,并保留原文出处.内容若有错误或补充,请联系:szyzln@126.com 本文主要讲解如何在已有官方Ce ...
- Centos7.5的定制化安装
一.前言 关于定制化centos7.5的镜像真的是历经波折,前前后后.来来回回尝试了不少于20次,上网找了各种关于定制7系统的方法,都没有成功... 但最终功夫不负有心人终于解决了,O(∩_∩)O哈哈 ...
随机推荐
- linux服务器系统负载监控-shell脚本
一.监控服务器系统负载情况: 1.用uptime命令查看当前负载情况(1分钟,5分钟,15分钟平均负载情况) # uptime 15:43:59 up 186 days, 20:04, 1 us ...
- FortiGate下视频会议等语音相关配置
关闭老的基于会话的alg机制(即删除session-helper中的SIP条目) config system session-helper delete 13 #删除sip end
- 在IDEA中使用MyBatis Generator自动生成代码
转载自 https://blog.csdn.net/hua_faded/article/details/78900780 一.配置Maven pom.xml 文件 在pom.xml增加以下插件: ...
- Linux下安装Hadoop
第一步: Hadoop需要JAVA的支持,所以需要先安装JAVA 查看是否已安装JAVA 查看命令: java -version JRE(Java Runtime Environment),它是你运行 ...
- APIView流程——请求方式分发
- 64位版本为什么叫amd64,而不是intel64
64位版本为什么叫amd64,而不是intel64? 首先了解下常见的几个架构: X86是一个指令集,是刚有个人电脑时候的什么8086,286,386的那个兼容的指令集. “x86-64”,有时会 ...
- 2019.02.19 bzoj2655: calc(生成函数+拉格朗日插值)
传送门 题意简述:问有多少数列满足如下条件: 所有数在[1,A][1,A][1,A]之间. 没有相同的数 数列长度为nnn 一个数列的贡献是所有数之积,问所有满足条件的数列的贡献之和. A≤1e9,n ...
- Linux下强制杀死进程的方法
常规篇: 首先,用ps查看进程,方法如下: $ ps -ef …… smx 1822 1 0 11:38 ? 00:00:49 gnome-terminal smx 1823 1822 0 11:38 ...
- HTML学习总结(作业五)
1:HTML简介 超文本标记语言,标准通用标记语言下的一个应用.是 网页制作必备的编程语言“超文本”就是指页面内可以包含图片.链接,甚至音乐.程序等非文字元素.超文本标记语言的结构包括“头”部分(英语 ...
- Alpha冲刺-(9/10)
Part.1 开篇 队名:彳艮彳亍团队 组长博客:戳我进入 作业博客:班级博客本次作业的链接 Part.2 成员汇报 组员1(组长)柯奇豪 过去两天完成了哪些任务 进一步优化代码,结合自己负责的部分修 ...