转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/41087219,本文出自:【张鸿洋的博客】

1、概述

学习Android少不了模仿各种app的界面,自从微信6.0问世以后,就觉得微信切换时那个变色的Tab图标屌屌的,今天我就带大家自定义控件,带你变色变得飞起~~

好了,下面先看下效果图:

清晰度不太好,大家凑合看~~有木有觉得这个颜色弱爆了了的,,,下面我动动手指给你换个颜色:

有没有这个颜色比较妖一点~~~好了~下面开始介绍原理。

2、原理介绍

通过上面的效果图,大家可能也猜到了,我们的图标并非是两张图片,而是一张图,并且目标颜色是可定制的,谁让现在动不动就谈个性化呢。

那么我们如何做到,可以让图标随心所遇的变色了,其实原理,在我的博客中出现了很多次了,下面你将看到一张熟悉的图:

有没有很熟悉的感脚,我们实际上还是利用了Paint的Xfermode,这次我们使用的是:Mode.DST_IN

Dst_IN回顾一下什么效果,先绘制Dst,设置Mode,再绘制Src,则显示的是先后绘图的交集区域,且是Dst.

再仔细观察下我们的图标:

为了方便大家的观看,我特意拿ps选择了一下我们图标的非透明区域,可以看到,我们这个小机器人非透明区域就是被线框起来的部分。

然后,我们图标变色的原理就出现了:

1、先绘制一个颜色(例如:粉红)

2、设置Mode=DST_IN

3、绘制我们这个可爱的小机器人

回答我,显示什么,是不是显示交集,交集是什么?交集是我们的小机器人的非透明区域,也就是那张脸,除了两个眼;

好了,那怎么变色呢?

我绘制一个颜色的时候,难道不能设置alpha么~~~

到此,大家应该已经了解了我们图标的绘制的原理了吧。

如果你对Mode不熟悉:建议移步至:Android 自定义控件实现刮刮卡效果 真的就只是刮刮卡么

3、自定义图标控件

我们的整个界面不用说,是ViewPager+Fragment ,现在关注的是底部~~

接下来我们考虑,底部的Tab,Tab我们的布局是LinearLayout,内部四个View,通过设置weight达到均分~~

这个View就是我们的自定义的图标控件了,我们叫做:ChangeColorIconWithTextView

接下来考虑,应该有什么属性公布出来

1、自定义属性

想了一下,我决定把图标,图标颜色,图标下显示的文字,文字大小这四个属性作为自定义属性。

那就自定义属性走起了:

a、values/attr.xml

<?xml version="1.0" encoding="utf-8"?>
<resources> <attr name="icon" format="reference" />
<attr name="color" format="color" />
<attr name="text" format="string" />
<attr name="text_size" format="dimension" /> <declare-styleable name="ChangeColorIconView">
<attr name="icon" />
<attr name="color" />
<attr name="text" />
<attr name="text_size" />
</declare-styleable> </resources>

b、在布局文件中使用

 <com.zhy.weixin6.ui.ChangeColorIconWithTextView
android:id="@+id/id_indicator_one"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="1"
android:padding="5dp"
zhy:icon="@drawable/ic_menu_start_conversation"
zhy:text="@string/tab_weixin"
zhy:text_size="12sp" />

自己注意命名空间的写法,xmlns:zhy="http://schemas.android.com/apk/res/应用的包名"。

c、在构造方法中获取

public class ChangeColorIconWithTextView extends View
{ private Bitmap mBitmap;
private Canvas mCanvas;
private Paint mPaint;
/**
* 颜色
*/
private int mColor = 0xFF45C01A;
/**
* 透明度 0.0-1.0
*/
private float mAlpha = 0f;
/**
* 图标
*/
private Bitmap mIconBitmap;
/**
* 限制绘制icon的范围
*/
private Rect mIconRect;
/**
* icon底部文本
*/
private String mText = "微信";
private int mTextSize = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 10, getResources().getDisplayMetrics());
private Paint mTextPaint;
private Rect mTextBound = new Rect(); public ChangeColorIconWithTextView(Context context)
{
super(context);
} /**
* 初始化自定义属性值
*
* @param context
* @param attrs
*/
public ChangeColorIconWithTextView(Context context, AttributeSet attrs)
{
super(context, attrs); // 获取设置的图标
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.ChangeColorIconView); int n = a.getIndexCount();
for (int i = 0; i < n; i++)
{ int attr = a.getIndex(i);
switch (attr)
{
case R.styleable.ChangeColorIconView_icon:
BitmapDrawable drawable = (BitmapDrawable) a.getDrawable(attr);
mIconBitmap = drawable.getBitmap();
break;
case R.styleable.ChangeColorIconView_color:
mColor = a.getColor(attr, 0x45C01A);
break;
case R.styleable.ChangeColorIconView_text:
mText = a.getString(attr);
break;
case R.styleable.ChangeColorIconView_text_size:
mTextSize = (int) a.getDimension(attr, TypedValue
.applyDimension(TypedValue.COMPLEX_UNIT_SP, 10,
getResources().getDisplayMetrics()));
break; }
} a.recycle(); mTextPaint = new Paint();
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(0xff555555);
// 得到text绘制范围
mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBound); }

可以看到,我们在构造方法中获取了自定义的属性,并且计算了文本占据的控件存在我们的mTextBound中。

2、图标的绘制区域的选择

我们考虑下,有了属性,我们需要绘制一个文本,文本之上一个图标,我们怎么去控制绘制的区域呢?
我们的View显示区域,无非以下三种情况:

针对这三种情况,我门的图标的边长应该是什么呢?

我觉得边长应该是:控件的高度-文本的高度-内边距   与  控件的宽度-内边距  两者的小值;大家仔细推敲一下;

好了,有了上面的边长的结论,我们就开始计算图标的绘制范围了:

	@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 得到绘制icon的宽
int bitmapWidth = Math.min(getMeasuredWidth() - getPaddingLeft()
- getPaddingRight(), getMeasuredHeight() - getPaddingTop()
- getPaddingBottom() - mTextBound.height()); int left = getMeasuredWidth() / 2 - bitmapWidth / 2;
int top = (getMeasuredHeight() - mTextBound.height()) / 2 - bitmapWidth
/ 2;
// 设置icon的绘制范围
mIconRect = new Rect(left, top, left + bitmapWidth, top + bitmapWidth); }

3、绘制图标

绘制图标有很多步骤呀,我来列一列

1、计算alpha(默认为0)

2、绘制原图

3、在绘图区域,绘制一个纯色块(设置了alpha),此步绘制在内存的bitmap上

4、设置mode,针对内存中的bitmap上的paint

5、绘制我们的图标,此步绘制在内存的bitmap上

6、绘制原文本

7、绘制设置alpha和颜色后的文本

8、将内存中的bitmap绘制出来

根据上面的步骤,可以看出来,我们的图标其实绘制了两次,为什么要绘制原图呢,因为我觉得比较好看。

3-5步骤,就是我们上面分析的原理

6-7步,是绘制文本,可以看到,我们的文本就是通过设置alpha实现的

@Override
protected void onDraw(Canvas canvas)
{ int alpha = (int) Math.ceil((255 * mAlpha));
canvas.drawBitmap(mIconBitmap, null, mIconRect, null);
setupTargetBitmap(alpha);
drawSourceText(canvas, alpha);
drawTargetText(canvas, alpha);
canvas.drawBitmap(mBitmap, 0, 0, null); } private void setupTargetBitmap(int alpha)
{
mBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(),
Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
mPaint = new Paint();
mPaint.setColor(mColor);
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setAlpha(alpha);
mCanvas.drawRect(mIconRect, mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
mPaint.setAlpha(255);
mCanvas.drawBitmap(mIconBitmap, null, mIconRect, mPaint);
} private void drawSourceText(Canvas canvas, int alpha)
{
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(0xff333333);
mTextPaint.setAlpha(255 - alpha);
canvas.drawText(mText, mIconRect.left + mIconRect.width() / 2
- mTextBound.width() / 2,
mIconRect.bottom + mTextBound.height(), mTextPaint);
} private void drawTargetText(Canvas canvas, int alpha)
{
mTextPaint.setColor(mColor);
mTextPaint.setAlpha(alpha);
canvas.drawText(mText, mIconRect.left + mIconRect.width() / 2
- mTextBound.width() / 2,
mIconRect.bottom + mTextBound.height(), mTextPaint); }

关于绘制文本区域的计算,首先是起点x:mIconRect.left + mIconRect.width() / 2- mTextBound.width() / 2 有点长哈,文本mIconRect.left + mIconRect.width() / 2这个位置,在图标水平区域的中心点,这个应该没有疑问;图标水平区域的中点- mTextBound.width() / 2 开始绘制文本,是不是就是居中在图标的下面;

有人可能会问:你怎么知道文本宽度小于图标,我有5个字咋办?5个字怎么了,照样是居中显示,不信你试试~~

4、公布设置透明度的方法

到此,我们的图标控件写完了,但是还没有把我们的控制icon的方法放出去:

	public void setIconAlpha(float alpha)
{
this.mAlpha = alpha;
invalidateView();
} private void invalidateView()
{
if (Looper.getMainLooper() == Looper.myLooper())
{
invalidate();
} else
{
postInvalidate();
}
}

我们叫做setIconAlpha,避免了和setAlpha冲突,设置完成后,invalidate一下~~~

到此就真的结束了,接下来看用法。

4、实战

1、布局文件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:zhy="http://schemas.android.com/apk/res/com.zhy.weixin6.ui"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" > <android.support.v4.view.ViewPager
android:id="@+id/id_viewpager"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1" >
</android.support.v4.view.ViewPager> <LinearLayout
android:layout_width="fill_parent"
android:layout_height="60dp"
android:background="@drawable/tabbg"
android:orientation="horizontal" > <com.zhy.weixin6.ui.ChangeColorIconWithTextView
android:id="@+id/id_indicator_one"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="1"
android:padding="5dp"
zhy:icon="@drawable/ic_menu_start_conversation"
zhy:text="@string/tab_weixin"
zhy:text_size="12sp" /> <com.zhy.weixin6.ui.ChangeColorIconWithTextView
android:id="@+id/id_indicator_two"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="1"
android:padding="5dp"
zhy:icon="@drawable/ic_menu_friendslist"
zhy:text="@string/tab_contact"
zhy:text_size="12sp" /> <com.zhy.weixin6.ui.ChangeColorIconWithTextView
android:id="@+id/id_indicator_three"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="1"
android:padding="5dp"
zhy:icon="@drawable/ic_menu_emoticons"
zhy:text="@string/tab_find"
zhy:text_size="12sp" /> <com.zhy.weixin6.ui.ChangeColorIconWithTextView
android:id="@+id/id_indicator_four"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="1"
android:padding="5dp"
zhy:icon="@drawable/ic_menu_allfriends"
zhy:text="@string/tab_me"
zhy:text_size="12sp" />
</LinearLayout> </LinearLayout>

2、MainActivity

package com.zhy.weixin6.ui;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List; import android.annotation.SuppressLint;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewConfiguration;
import android.view.Window; @SuppressLint("NewApi")
public class MainActivity extends FragmentActivity implements
OnPageChangeListener, OnClickListener
{
private ViewPager mViewPager;
private List<Fragment> mTabs = new ArrayList<Fragment>();
private FragmentPagerAdapter mAdapter; private String[] mTitles = new String[] { "First Fragment!",
"Second Fragment!", "Third Fragment!", "Fourth Fragment!" }; private List<ChangeColorIconWithTextView> mTabIndicator = new ArrayList<ChangeColorIconWithTextView>(); @Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); setOverflowShowingAlways();
getActionBar().setDisplayShowHomeEnabled(false);
mViewPager = (ViewPager) findViewById(R.id.id_viewpager); initDatas(); mViewPager.setAdapter(mAdapter);
mViewPager.setOnPageChangeListener(this);
} private void initDatas()
{ for (String title : mTitles)
{
TabFragment tabFragment = new TabFragment();
Bundle args = new Bundle();
args.putString("title", title);
tabFragment.setArguments(args);
mTabs.add(tabFragment);
} mAdapter = new FragmentPagerAdapter(getSupportFragmentManager())
{ @Override
public int getCount()
{
return mTabs.size();
} @Override
public Fragment getItem(int arg0)
{
return mTabs.get(arg0);
}
}; initTabIndicator(); } @Override
public boolean onCreateOptionsMenu(Menu menu)
{
getMenuInflater().inflate(R.menu.main, menu);
return true;
} private void initTabIndicator()
{
ChangeColorIconWithTextView one = (ChangeColorIconWithTextView) findViewById(R.id.id_indicator_one);
ChangeColorIconWithTextView two = (ChangeColorIconWithTextView) findViewById(R.id.id_indicator_two);
ChangeColorIconWithTextView three = (ChangeColorIconWithTextView) findViewById(R.id.id_indicator_three);
ChangeColorIconWithTextView four = (ChangeColorIconWithTextView) findViewById(R.id.id_indicator_four); mTabIndicator.add(one);
mTabIndicator.add(two);
mTabIndicator.add(three);
mTabIndicator.add(four); one.setOnClickListener(this);
two.setOnClickListener(this);
three.setOnClickListener(this);
four.setOnClickListener(this); one.setIconAlpha(1.0f);
} @Override
public void onPageSelected(int arg0)
{
} @Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels)
{
// Log.e("TAG", "position = " + position + " , positionOffset = "
// + positionOffset); if (positionOffset > 0)
{
ChangeColorIconWithTextView left = mTabIndicator.get(position);
ChangeColorIconWithTextView right = mTabIndicator.get(position + 1); left.setIconAlpha(1 - positionOffset);
right.setIconAlpha(positionOffset);
} } @Override
public void onPageScrollStateChanged(int state)
{ } @Override
public void onClick(View v)
{ resetOtherTabs(); switch (v.getId())
{
case R.id.id_indicator_one:
mTabIndicator.get(0).setIconAlpha(1.0f);
mViewPager.setCurrentItem(0, false);
break;
case R.id.id_indicator_two:
mTabIndicator.get(1).setIconAlpha(1.0f);
mViewPager.setCurrentItem(1, false);
break;
case R.id.id_indicator_three:
mTabIndicator.get(2).setIconAlpha(1.0f);
mViewPager.setCurrentItem(2, false);
break;
case R.id.id_indicator_four:
mTabIndicator.get(3).setIconAlpha(1.0f);
mViewPager.setCurrentItem(3, false);
break; } } /**
* 重置其他的Tab
*/
private void resetOtherTabs()
{
for (int i = 0; i < mTabIndicator.size(); i++)
{
mTabIndicator.get(i).setIconAlpha(0);
}
} @Override
public boolean onMenuOpened(int featureId, Menu menu)
{
if (featureId == Window.FEATURE_ACTION_BAR && menu != null)
{
if (menu.getClass().getSimpleName().equals("MenuBuilder"))
{
try
{
Method m = menu.getClass().getDeclaredMethod(
"setOptionalIconsVisible", Boolean.TYPE);
m.setAccessible(true);
m.invoke(menu, true);
} catch (Exception e)
{
}
}
}
return super.onMenuOpened(featureId, menu);
} private void setOverflowShowingAlways()
{
try
{
// true if a permanent menu key is present, false otherwise.
ViewConfiguration config = ViewConfiguration.get(this);
Field menuKeyField = ViewConfiguration.class
.getDeclaredField("sHasPermanentMenuKey");
menuKeyField.setAccessible(true);
menuKeyField.setBoolean(config, false);
} catch (Exception e)
{
e.printStackTrace();
}
} }

Activity里面代码虽然没什么注释,但是很简单哈,就是初始化Fragment,得到我们的适配器,然后设置给ViewPager;

initTabIndicator我们初始化我们的自定义控件,以及加上了点击事件;

唯一一个需要指出的就是:

我们在onPageScrolled中,动态的获取position以及positionOffset,然后拿到左右两个View,设置positionOffset ;

这里表示下惭愧,曾经在高仿微信5.2.1主界面架构 包含消息通知 的onPageScrolled中写了一堆的if else,在视频上线后,也有同学立马就提出了,一行代码搞定~~

所以,我们这里简单找了下规律,已经没有if else的身影了~~~

还剩两个反射的方法,是控制Actionbar的图标的,和点击menu按键,将ActionBar的menu显示在正常区域的~~

3、TabFragment

package com.zhy.weixin6.ui;

import android.graphics.Color;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView; public class TabFragment extends Fragment
{
private String mTitle = "Default"; public TabFragment()
{
} @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
if (getArguments() != null)
{
mTitle = getArguments().getString("title");
} TextView textView = new TextView(getActivity());
textView.setTextSize(20);
textView.setBackgroundColor(Color.parseColor("#ffffffff"));
textView.setGravity(Gravity.CENTER);
textView.setText(mTitle);
return textView;
}
}

好了,到此我们的整个案例就结束了~~

大家可以在布局文件中设置各种颜色,4个不同颜色也可以,尽情的玩耍吧~~

源码点击下载

我建了一个QQ群,方便大家交流。群号:55032675

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

博主部分视频已经上线,如果你不喜欢枯燥的文本,请猛戳(初录,期待您的支持):

1、Android 自定义控件实战 电商活动中的刮刮卡

2、Android自定义控件实战  打造Android流式布局和热门标签

3、Android智能机器人“小慕”的实现

4、高仿QQ5.0侧滑

5、高仿微信5.2.1主界面及消息提醒

版权声明:本文为博主原创文章,未经博主允许不得转载。

Android 高仿微信6.0主界面 带你玩转切换图标变色的更多相关文章

  1. Android 高仿微信实时聊天 基于百度云推送

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38799363 ,本文出自:[张鸿洋的博客] 一直在仿微信界面,今天终于有幸利用百 ...

  2. Android 高仿微信即时聊天 百度云为基础的推

    转载请注明出处:http://blog.csdn.net/lmj623565791/article/details/38799363 ,本文出自:[张鸿洋的博客] 一直在仿微信界面,今天最终有幸利用百 ...

  3. android高仿微信拍照、多选、预览、删除(去除相片)相冊功能

    先声明授人与鱼不如授人与渔,仅仅能提供一个思路,当然须要源代码的同学能够私下有偿问我要源代码:QQ:508181017 工作了将近三年时间了,一直没正儿八经的研究系统自带的相冊和拍照,这回来个高仿微信 ...

  4. Android 高仿微信头像截取 打造不一样的自定义控件

    转载请表明出处:http://blog.csdn.net/lmj623565791/article/details/39761281,本文出自:[张鸿洋的博客] 1.概述 前面已经写了关于检测手势识别 ...

  5. Android 高仿微信支付键盘

    现在很多app的支付.输入密码功能,都已经开始使用自定义数字键盘,不仅更加方便.其效果着实精致. 下面带着大家学习下,如何高仿微信的数字键盘,可以拿来直接用在自身的项目中. 先看下效果图: 1. 自定 ...

  6. Android 高仿微信语音聊天页面高斯模糊效果

    目前的应用市场上,使用毛玻璃效果的APP随处可见,比如用过微信语音聊天的人可以发现,语音聊天页面就使用了高斯模糊效果. 先看下效果图: 仔细观察上图,我们可以发现,背景图以用户头像为模板,对其进行了高 ...

  7. Android高仿微信(一)——如何消除启动时的白屏

    默认情况下,APP启动时会先把屏幕刷成白色,然后才绘制第一个Activity中的View,这两个步骤之间的延迟会造成启动后先看到白屏(时间大概为1秒左右).时间不长,但是我们也看到,一般的APP时不存 ...

  8. Android 高仿微信朋友圈动态, 支持双击手势放大并滑动查看图片。

    转载请注明出处:http://blog.csdn.net/sk719887916/article/details/40348873 作者skay: 最近参与了开发一款旅行APP,其中包含实时聊天和动态 ...

  9. Android 高仿微信支付密码输入控件

    像微信支付密码控件,在app中是一个多么司空见惯的功能.最近,项目需要这个功能,于是乎就实现这个功能. 老样子,投篮需要找准角度,变成需要理清思路.对于这个"小而美"的控件,我们思 ...

随机推荐

  1. 史上最简单的C语言链表实现,没有之一

    #include <stdio.h> #include <string.h> #include <stdlib.h> #define NR(x) (sizeof(x ...

  2. Which SQL statement is the trump card to the senior software developer

    Which SQL statement is the trump card to the senior software developer                    MA Genfeng ...

  3. was上的应用程序部分启动的原因

    最近几天为了方便联调,我把两个项目配置到was测试环境上,前几天还好好的,昨天忽然有一个项目反复安装后都呈现部分启动的状态,打开节点一看,偏偏没启动的那个节点就是我需要用的79节点. 这让我很郁闷,硬 ...

  4. (译)WebRTC实战: STUN, TURN, Signaling

    http://xiaol.me/2014/08/24/webrtc-stun-turn-signaling/ 原文:WebRTC in the real world: STUN, TURN and s ...

  5. hadoop 2.x安装:完全分布式安装

    1. 安装环境 本文使用三台CentOS6.4虚拟机模拟完全分布式环境.前五个过程和hadoop1.x安装相同 1.1. 安装环境 项目 参数 主操作系统 Windows 10 64 bit,8GB内 ...

  6. linux/unix解压缩

    转自:http://blog.sina.com.cn/s/blog_6f2d29af01015ac6.html zip: 压缩: zip [-AcdDfFghjJKlLmoqrSTuvVwXyz$][ ...

  7. 学习Timer定时器

    原文地址:http://www.cppblog.com/ivenher/articles/19969.html setTimer函数用于创建一个计时器,KillTimer函数用于销毁一个计时器.计时器 ...

  8. Coursera-AndrewNg(吴恩达)机器学习笔记——第三周编程作业

    一. 逻辑回归 1.背景:使用逻辑回归预测学生是否会被大学录取. 2.首先对数据进行可视化,代码如下: pos = find(y==); %找到通过学生的序号向量 neg = find(y==); % ...

  9. python笔记:#011#循环

    循环 目标 程序的三大流程 while 循环基本使用 break 和 continue while 循环嵌套 01. 程序的三大流程 在程序开发中,一共有三种流程方式: 顺序 -- 从上向下,顺序执行 ...

  10. win10汇编环境的搭建

    第一步:下载DOSBox0.74-win32-installer 可以去官网:http://www.dosbox.com/ 或者链接:https://pan.baidu.com/s/1UA77qTLO ...