Android -- 打造我们的StepView
1,前两天我们分析了Github开源的StepView 《自定义StepView实现个人信息验证进度条》,这两天想着想自己写一个,so,就有了这一篇文章,不废话,先看看实现的效果:

2,首先我们来看看我们常规的自定义view的基础步骤吧
1,继承View,重写构造方法
2,自定义属性
3,重写onMeasure()测量控件高度
4,重写onDraw()绘制子view
- 初步分析
首先根据我们的上面效果,可以看到,主要是由直线、圆环、下面的文字组成,所以我们打算使用这三种view组合来形成我们上面的效果
- 准备工作
①首先我们要提供一个装置下面文字的集合texts,我们文字有文字的大小属性mTextSize、正常文字颜色mColorTextDefault、文字被选中时的颜色mColorTextSelect,最后还有文字距离上面圆环的距离mMarginTop
②然后我们提供相关的圆环相关的属性,圆的半径mCircleRadius、圆环被选中的颜色mColorCircleSelect、圆环正常时的颜色mColorCircleDefault
③再看看我们链接圆弧之间的直线属性,直线的长度mLineLength、直线的高度mLineHeight,颜色和我们圆环默认颜色相同,就不用重新定义了
④还有一些需要定义的属性,例如当前被选中的位置mSelectPosition,每一个测量的TextView保存的Rect的集合mBounds,还有各种画笔
所以我们就可以开始写一写代码了,首先创建StepView继承View,然后初始化数据,并测量TextView,将测量信息保存在mBounds集合中
public class SlideStepView extends View {
//先分析我们这次需要哪些预备的属性
//存放下面文字集合
private List<String> texts;
//文字大小
private int mTextSize;
//文字常规颜色
private int mColorTextDefault;
//文字被选择时候的颜色
private int mColorTextSelect;
//圆和文字之间的距离
private int mMarginTop;
//线段和圆圈常规的颜色
private int mColorCircleDefault;
//圆圈被选中的的颜色
private int mColorCircleSelect;
//中间线段的整个长度
private float mLineLength;
//中间线段宽度
private int mLineHeight;
//圆圈的半径
private int mCircleRadius;
//选中后蓝色的宽度
private int mSelectCircleStroke;
//当前选中的下标
private int mSelectPosition;
//保存每个TextView的测量矩形数据
private List<Rect> mBounds;
//各种画笔
private Paint mTextPaint;
private Paint mLinePaint;
private Paint mCirclePaint;
private Paint mCircleSelectPaint;
public SlideStepView(Context context) {
this(context, null);
}
public SlideStepView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SlideStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化数基本属性
init();
}
private void init() {
//初始化数据源容器
texts = new ArrayList<>();
mBounds = new ArrayList<>();
//添加加数据
texts.add("订单已支付");
texts.add("商家已接单");
texts.add("骑手已接单");
texts.add("订单已送达");
//将当前选中为2
mSelectPosition = 1;
mMarginTop = 20;
mCircleRadius = 30;
mSelectCircleStroke = 3;
//初始化文字属性
mColorTextDefault = Color.GRAY;
mColorTextSelect = Color.BLUE;
mTextSize = 20;
mTextPaint = new Paint();
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mColorTextDefault);
mTextPaint.setAntiAlias(true);
//初始化圆圈属性
mColorCircleDefault = Color.argb(255, 234, 234, 234);
mCirclePaint = new Paint();
mCirclePaint.setColor(mColorCircleDefault);
mCirclePaint.setStyle(Paint.Style.FILL);
mCirclePaint.setAntiAlias(true);
//初始化被选中的圆圈
mColorCircleSelect = Color.BLUE;
mCircleSelectPaint = new Paint();
mCircleSelectPaint.setColor(mColorCircleSelect);
mCircleSelectPaint.setStyle(Paint.Style.FILL);
mCircleSelectPaint.setAntiAlias(true);
// mCircleSelectPaint.setStrokeWidth(mSelectCircleStroke);
//设置线段属性
mLineHeight = 5;
mLinePaint = new Paint();
mLinePaint.setColor(mColorCircleDefault);
mLinePaint.setStyle(Paint.Style.FILL);
mLinePaint.setStrokeWidth(mLineHeight);
mLinePaint.setAntiAlias(true);
//测量TextView
measureText();
}
private void measureText() {
for (int i = 0; i < texts.size(); i++) {
Rect rect = new Rect();
mTextPaint.getTextBounds(texts.get(i), 0, texts.get(i).length(), rect);
mBounds.add(rect);
}
}
}
然后在onChangeSize中计算出mLineLength的长度(这里很简单 getWidth() - paddingLeft -paddingRight -2*mCircleRadius),重写onDraw()方法
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh); //计算线段整条线段长度(总控件宽度 - Padding - 最左边和最右边的两个圆的直径)
mLineLength = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleRadius * 2;
} /**
* 绘制view
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
//绘制线条
canvas.drawLine(mCircleRadius, mCircleRadius, getWidth() - mCircleRadius, mCircleRadius, mLinePaint); //开是循环绘制view
for (int i = 0; i < texts.size(); i++) {
mTextPaint.setColor(mColorCircleDefault);
if (mSelectPosition == i) {
//绘制选中的圆圈
canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCircleSelectPaint);
mTextPaint.setColor(mColorCircleSelect);
} else {
//绘制默中的圆圈
canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCirclePaint);
}
//绘制文字
int startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop();
if (i == 0) {
canvas.drawText(texts.get(i), 0, startTextY, mTextPaint);
} else if (i == texts.size() - 1) {
canvas.drawText(texts.get(i), getWidth() - mBounds.get(i).width(), startTextY, mTextPaint);
} else {
canvas.drawText(texts.get(i), mCircleRadius + ((mLineLength / (texts.size() - 1)) * i) - (mBounds.get(i).width() / 2), startTextY, mTextPaint);
}
}
}
在布局文件引用
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dip"> <com.qianmo.activitydetail.view.SlideStepView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#aaff0000"/>
</LinearLayout>
这样应该可以实现基本效果了,看一看我们实现的效果

- 重写onMeasure,改变测量的高度
这里我们可以看到当我们设置我们控件的高度为wrap_content,控件缺填充了整个屏幕,这一点我们在之前的《onMeasure()源码分析》写过,没有了解过的同学,大家可以去看一下,所以我们要修改onMeasure中的方法
/**
* 重写测量方式
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int height;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = mMarginTop + 2 * mCircleRadius + mBounds.get(0).height();
//高度
Log.i("wangjitao:", "mMarginTop:" + mMarginTop + ",mCircleRadius:" + mCircleRadius + ",mBounds:"
+ mBounds.get(0).height() + ",height" + height);
}
//保存测量结果
setMeasuredDimension(widthSize, height);
}
再看一下我们的运行效果

- 对canvas.drawText()方法进行理解
我们这时候将我们前面的init()方法中的mMarginTop修改为0,mMarginTop代表下面文字距离上面圆环的距离,设置为0的话就表示我们的文字的text刚好贴在这个圆环的下面,但是实际效果不是这个样子的,看一下运行的效果

这里我们可以看到我们的文字和我们的圆弧重叠了,这是为什么呢? 我们的代码逻辑也问题啊,为什么会出现这个问题呢?我们下来看一下下面这张text的展示图就知道了

上面所有的属性都被封装在FontMetrics类中,通过它可以获取并计算文本的宽高,大体翻译一下,可能不准确;
top:在一个大小确定的字体中,被当做最高字形,基线(base)上方的最大距离。
ascent:单行文本中,在基线(base)上方被推荐的距离。
descent:单行文本中,在基线(base)下方被推荐的距离。
bottom:在一个大小确定的字体中,被当做最低字形,基线(base)下方的最大距离。
这是我们自定义View中text的一些属性,有人会问,楼猪啊 ,为什么要让我们了解这个些知识呢?因为我们的上面出的重叠问题就是这一点的问题,在我们的正常思维的认知中我们的canvas.drawText的第三个参数是Y坐标的起始点,而我们上面的代码Y坐标的计算方式是 startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop();我们的主观思维也感觉没问题,但是让我们看一下canvas.drawText()方法的源码
/**
* Draw the text, with origin at (x,y), using the specified paint. The
* origin is interpreted based on the Align setting in the paint.
*
* @param text The text to be drawn
* @param x The x-coordinate of the origin of the text being drawn
* @param y The y-coordinate of the baseline of the text being drawn
* @param paint The paint used for the text (e.g. color, size, style)
*/
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
native_drawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags,
paint.getNativeInstance(), paint.mNativeTypeface);
}
看到没有“@param y The y-coordinate of the baseline of the text being drawn” 这个方法中我们的y参数表示我们的baseline,而不是我们之前的想当然的test的top属性,所以我们要修改startTextY 的计算方式为
//这里要对基线进行理解
int startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop(); //以前
Log.i("wangjitao", "以前:" + startTextY);
//现在是这样的,首先获取基线对象
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
startTextY = getHeight() - (int) fontMetrics.bottom;
ok,再看看我们的运行效果

没什么问题了
- 重写onTouch()方实现侧滑更换当前选中位置
这个没什么好讲的,就是向左滑动和向右滑动改变当前选中位置而已,代码如下:
private float downX;
private float upX; @Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
//按下手指的时候记录下按下的位置
case MotionEvent.ACTION_DOWN:
Log.e("wangjitao", "手指按下: getX:" + downX);
downX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
Log.i("wangjitao", "手指滑动: ");
break;
case MotionEvent.ACTION_UP:
upX = event.getX();
Log.e("wangjitao", "手指抬起: " + upX);
if (downX - upX > 50) {
downX = 0;
upX = 0;
//向左滑动
//判断做滑动的时候当前选择点时候在在初始状态下
if (mSelectPosition != 0) {
//更新view
mSelectPosition--;
} else {
mSelectPosition = texts.size() - 1;
}
invalidate();
} else if (upX - downX > 50) {
//向右滑动
downX = 0;
upX = 0;
//判断做滑动的时候当前选择点时候在最后一个点上
if (mSelectPosition != texts.size() - 1) {
//更新view
mSelectPosition++;
} else {
mSelectPosition = 0;
}
invalidate();
} else {
downX = 0;
upX = 0;
}
break;
}
return true;
}
再把最后所有的代码贴出来
package com.qianmo.activitydetail.view; import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View; import java.util.ArrayList;
import java.util.List; /**
* Created by wangjitao on 2017/3/24 0024.
* E-Mail:543441727@qq.com
* 自定义view实现StepView的实现
*/ public class SlideStepView extends View {
//先分析我们这次需要哪些预备的属性 //存放下面文字集合
private List<String> texts;
//文字大小
private int mTextSize;
//文字常规颜色
private int mColorTextDefault;
//文字被选择时候的颜色
private int mColorTextSelect;
//圆和文字之间的距离
private int mMarginTop;
//线段和圆圈常规的颜色
private int mColorCircleDefault;
//圆圈被选中的的颜色
private int mColorCircleSelect;
//中间线段的整个长度
private float mLineLength;
//中间线段宽度
private int mLineHeight;
//圆圈的半径
private int mCircleRadius;
//选中后蓝色的宽度
private int mSelectCircleStroke;
//当前选中的下标
private int mSelectPosition; //保存每个TextView的测量矩形数据
private List<Rect> mBounds; //各种画笔
private Paint mTextPaint;
private Paint mLinePaint;
private Paint mCirclePaint;
private Paint mCircleSelectPaint; public SlideStepView(Context context) {
this(context, null);
} public SlideStepView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
} public SlideStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); //初始化数基本属性
init();
} private void init() {
//初始化数据源容器
texts = new ArrayList<>();
mBounds = new ArrayList<>(); //添加加数据
texts.add("订单已支付");
texts.add("商家已接单");
texts.add("骑手已接单");
texts.add("订单已送达"); //将当前选中为2
mSelectPosition = 1;
mMarginTop = 0;
mCircleRadius = 30;
mSelectCircleStroke = 3; //初始化文字属性
mColorTextDefault = Color.GRAY;
mColorTextSelect = Color.BLUE;
mTextSize = 20;
mTextPaint = new Paint();
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mColorTextDefault);
mTextPaint.setAntiAlias(true); //初始化圆圈属性
mColorCircleDefault = Color.argb(255, 234, 234, 234); mCirclePaint = new Paint();
mCirclePaint.setColor(mColorCircleDefault);
mCirclePaint.setStyle(Paint.Style.FILL);
mCirclePaint.setAntiAlias(true); //初始化被选中的圆圈
mColorCircleSelect = Color.BLUE;
mCircleSelectPaint = new Paint();
mCircleSelectPaint.setColor(mColorCircleSelect);
mCircleSelectPaint.setStyle(Paint.Style.FILL);
mCircleSelectPaint.setAntiAlias(true);
// mCircleSelectPaint.setStrokeWidth(mSelectCircleStroke); //设置线段属性
mLineHeight = 5;
mLinePaint = new Paint();
mLinePaint.setColor(mColorCircleDefault);
mLinePaint.setStyle(Paint.Style.FILL);
mLinePaint.setStrokeWidth(mLineHeight);
mLinePaint.setAntiAlias(true); //测量TextView
measureText();
} private void measureText() {
for (int i = 0; i < texts.size(); i++) {
Rect rect = new Rect();
mTextPaint.getTextBounds(texts.get(i), 0, texts.get(i).length(), rect);
mBounds.add(rect);
}
} /**
* 重写测量方式
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int height;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = mMarginTop + 2 * mCircleRadius + mBounds.get(0).height();
//高度
Log.i("wangjitao:", "mMarginTop:" + mMarginTop + ",mCircleRadius:" + mCircleRadius + ",mBounds:"
+ mBounds.get(0).height() + ",height" + height);
}
//保存测量结果
setMeasuredDimension(widthSize, height);
} @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh); //计算线段整条线段长度(总控件宽度 - Padding - 最左边和最右边的两个圆的直径)
mLineLength = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleRadius * 2;
} /**
* 绘制view
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
//绘制线条
canvas.drawLine(mCircleRadius, mCircleRadius, getWidth() - mCircleRadius, mCircleRadius, mLinePaint); //开是循环绘制view
for (int i = 0; i < texts.size(); i++) {
mTextPaint.setColor(mColorCircleDefault);
if (mSelectPosition == i) {
//绘制选中的圆圈
canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCircleSelectPaint);
mTextPaint.setColor(mColorCircleSelect);
} else {
//绘制默中的圆圈
canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCirclePaint);
}
//绘制文字
//这里要对基线进行理解
int startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop(); //以前
Log.i("wangjitao", "以前:" + startTextY);
//现在是这样的,首先获取基线对象
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
startTextY = getHeight() - (int) fontMetrics.bottom;
Log.i("wangjitao", "现在:" + startTextY);
if (i == 0) {
canvas.drawText(texts.get(i), 0, startTextY, mTextPaint);
} else if (i == texts.size() - 1) {
canvas.drawText(texts.get(i), getWidth() - mBounds.get(i).width(), startTextY, mTextPaint);
} else { canvas.drawText(texts.get(i), mCircleRadius + ((mLineLength / (texts.size() - 1)) * i) - (mBounds.get(i).width() / 2), startTextY, mTextPaint);
}
}
} private float downX;
private float upX; @Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
//按下手指的时候记录下按下的位置
case MotionEvent.ACTION_DOWN:
Log.e("wangjitao", "手指按下: getX:" + downX);
downX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
Log.i("wangjitao", "手指滑动: ");
break;
case MotionEvent.ACTION_UP:
upX = event.getX();
Log.e("wangjitao", "手指抬起: " + upX);
if (downX - upX > 50) {
downX = 0;
upX = 0;
//向左滑动
//判断做滑动的时候当前选择点时候在在初始状态下
if (mSelectPosition != 0) {
//更新view
mSelectPosition--;
} else {
mSelectPosition = texts.size() - 1;
}
invalidate();
} else if (upX - downX > 50) {
//向右滑动
downX = 0;
upX = 0;
//判断做滑动的时候当前选择点时候在最后一个点上
if (mSelectPosition != texts.size() - 1) {
//更新view
mSelectPosition++;
} else {
mSelectPosition = 0;
}
invalidate();
} else {
downX = 0;
upX = 0;
}
break;
}
return true;
}
}
运行效果

- 添加自定义属性
这里我们把好多控件的属性都写死了,我们可以用自定义属性来实现布局文件中动态的改变的,不了解的同学可以看我之前的《深入了解自定义属性》,这里就不一起写了,See You····
Android -- 打造我们的StepView的更多相关文章
- Android 打造炫目的圆形菜单 秒秒钟高仿建行圆形菜单
原文:Android 打造炫目的圆形菜单 秒秒钟高仿建行圆形菜单 转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/43131133, ...
- Android 打造自己的ImageLoader
Android 打造自己的ImageLoader 学习和参考 Android开发艺术探索 https://blog.csdn.net/column/details/15318.html 郭霖大神的Gl ...
- Android 打造完美的侧滑菜单/侧滑View控件
概述 Android 打造完美的侧滑菜单/侧滑View控件,完全自定义实现,支持左右两个方向弹出,代码高度简洁流畅,兼容性高,控件实用方便. 详细 代码下载:http://www.demodashi. ...
- Android打造属于自己的数据库操作类。
1.概述 开发Android的同学都知道sdk已经为我们提供了一个SQLiteOpenHelper类来创建和管理SQLite数据库,通过写一个子类去继承它,就可以方便的创建.管理数据库.但是当我们需要 ...
- android打造万能的适配器(转)
荒废了两天,今天与大家分享一个ListView的适配器 前段时间在学习慕课网的视频,觉得这种实现方式较好,便记录了下来,最近的项目中也使用了多次,节省了大量的代码,特此拿来与大家分享一下. 还是先看图 ...
- Android 打造形形色色的进度条 实现可以如此简单
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/43371299 ,本文出自:[张鸿洋的博客] 1.概述 最近需要用进度条,秉着不重 ...
- Android 打造任意层级树形控件 考验你的数据结构和设计
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40212367,本文出自:[张鸿洋的博客] 1.概述 大家在项目中或多或少的可能会 ...
- Android 打造编译时注解解析框架 这只是一个开始
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/43452969 ,本文出自:[张鸿洋的博客] 1.概述 记得很久以前,写过几篇博客 ...
- Android 打造任意层级树形控件 考验你的数据结构和设计
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40212367,本文出自:[张鸿洋的博客] 1.概述 大家在项目中或多或少的可能会 ...
随机推荐
- [dpdk][kni] dpdk kernel network interface
文档:https://doc.dpdk.org/guides/prog_guide/kernel_nic_interface.html 摘要: The KNI kernel loadable modu ...
- jquery图片懒加载效果
1.要引入jquery 2.要引入underscore.js <!DOCTYPE html> <html lang="en"> <head> & ...
- 【python基础】sys
sys模块 参考: https://blog.csdn.net/qq_38526635/article/details/81739321 http://www.cnblogs.com/cherishr ...
- <<Sklearn 与 TensorFlow 机器学习实用指南>>
地址 https://github.com/apachecn/hands-on-ml-zh 目录结构 零.前言 第一部分 机器学习基础 一.机器学习概览 二.一个完整的机器学习项目 三.分类 四.训练 ...
- oracle闪回的使用
1.闪回查询(原理:依赖于UNDO表空间)查询当前SCN号select current_scn from v$database;误删数据以后select * from table_name as of ...
- 张小龙2018PRO版微信公开课演讲全文 透露2018微信全新计划
大家好!我是张小龙.欢迎大家来到微信公开课. 刚刚出现的是我打游戏的画面,被大家看到了,那个不是我最好的水平,因为有点紧张,我最高分曾打到6000多分.当然我是练习了很久了,并不是我比大家更厉害,而是 ...
- 打开Delphi 10.1 berlin提示脚本错误的解决方法
HKEY_CURRENT_USER\SOFTWARE\Embarcadero\BDS\18.0\Known IDE Packages $(BDS)\Bin\CommunityToolbar240.bp ...
- ORACLE入门之Linux基础篇
VIM0 这是数字『0 』:移动到这一行的最前面字符处$ 移动到这一行的最后面字符处G 移动到这个档案的最后一行nG n 为数字.移动到这个档案的第n 行.例如20G 则会移动到这个档 ...
- tp5Auth权限实现
原文地址:https://blog.csdn.net/qq_33257081/article/details/79137190 下面本人为大家讲解一下如何实现auth权限, 第一步,新建Auth.ph ...
- golang gui library 库
andlabs/ui已经重写,稳定性增强,但是组件很少,只提供了几种基础的控件,慎用.gxui死了,别用.linuxdeepin转QT了,所以…… windows系统最好的选择是walk. 首先,写w ...