Android 简单音乐播放器开发
我把我最新的版本可以定时关闭,界面做了很大优化。GitHub上需要的可以下载:
https://github.com/DhyanaCoder/IMusic
————————————————————————————————————————
首先看一下我的项目结构:
可以看到iMuisc里的文件有MainActivity MusicPlayService ScanMusicUtil Song songApater timing timing timingstopService
文件。
- MainActivity是主活动文件
- MusicPlayService是运行Mediaplayer的服务文件
- ScanMusicUtil是搜索本地音乐的工具类
- Song是音乐的Java Bean类
- songAdpater是显示音乐列表的RecyclerView的适配器
- timing是设置定时的活动文件
- timingstopService是定时服务文件
首先分析 MainActivity 代码如下
public class MainActivity extends AppCompatActivity {private DrawerLayout mDrawerLayout;private songAdpater recyclerview_adapter;private List<Song> mSongList=new ArrayList<>();public static ImageButton play; //将Image设置为static以便音乐播放服务更改。public ImageButton last;public ImageButton next;public static int state=0;//音乐播放器的状态 1为正在播放 0为处于暂停或者未初始化public TextView showSongName;public MusicPlayService.MusicServiceBinder musicServiceBinder;private ServiceConnection connection=new ServiceConnection() {//1@Overridepublic void onServiceConnected(ComponentName name, IBinder service) { musicServiceBinder=(MusicPlayService.MusicServiceBinder)service;musicServiceBinder.InitBinder(mSongList);}@Overridepublic void onServiceDisconnected(ComponentName name) {}};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//以下代码隐藏标题栏ActionBar actionBar =getSupportActionBar();if(actionBar!=null){actionBar.hide();}final Intent intent=new Intent(this,MusicPlayService.class);IntentFilter intentFilter=new IntentFilter();intentFilter.addAction("com.example.iMusic.UI_UPDATE");MyBroadcast my=new MyBroadcast();registerReceiver(my,intentFilter);if( bindService(intent,connection,BIND_AUTO_CREATE)){Log.d("bindservice","success");}else{Log.d("bindservice","failed");}startService(intent);if(musicServiceBinder==null)Log.d("testest!","mbinder is null");showSongName=(TextView)findViewById(R.id.show_songname);play=(ImageButton) findViewById(R.id.music_play);last=(ImageButton) findViewById(R.id.music_left);next=(ImageButton) findViewById(R.id.music_right);last.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {musicServiceBinder.last();}});next.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {musicServiceBinder.next();}});play.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {if(state==1){musicServiceBinder.pause();play.setBackgroundResource(R.drawable.music_play);state=0;}else{play.setBackgroundResource(R.drawable.music_pause);musicServiceBinder.play();state=1;}}});mDrawerLayout=(DrawerLayout)findViewById(R.id.drawer_layout);NavigationView navigationView=(NavigationView)findViewById(R.id.nav_view);navigationView.setCheckedItem(R.id.setting1);navigationView.setCheckedItem(R.id.timing);navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {@Overridepublic boolean onNavigationItemSelected(@NonNull MenuItem item) {switch (item.getItemId()){case R.id.setting1:mDrawerLayout.closeDrawers();break;case R.id.timing:Intent timingIntent=new Intent(MainActivity.this,timing.class);startActivity(timingIntent);}return true;}});ImageButton imageButton=(ImageButton) findViewById(R.id.setting);imageButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {mDrawerLayout.openDrawer(GravityCompat.START);}});List<String> permissionList=new ArrayList<>();//构建权限申请表,以下就是逐步申请权限if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED){permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);}else{mSongList=ScanMusicUtil.scanMusic(this);}if(!permissionList.isEmpty() ){String [] permissions=permissionList.toArray(new String[permissionList.size()]);ActivityCompat.requestPermissions(MainActivity.this,permissions,1);}recyclerview_adapter=new songAdpater(mSongList,this);RecyclerView recyclerView_song=(RecyclerView)findViewById(R.id.recyclerview_song);recyclerView_song.setAdapter(recyclerview_adapter);LinearLayoutManager layoutManager=new LinearLayoutManager(this);layoutManager.setOrientation(LinearLayoutManager.VERTICAL);recyclerView_song.setLayoutManager(layoutManager);}public void onRequestPermissionsResult(int requestCode,String[] permissions,int[] grantResults){switch(requestCode){case 1:if(grantResults.length>0){for(int result:grantResults){if(result!=PackageManager.PERMISSION_GRANTED){Toast.makeText(this,"必须同意所有权限才能使用本程序",Toast.LENGTH_SHORT).show();finish();return;}}mSongList=ScanMusicUtil.scanMusic(this);recyclerview_adapter.notifyDataSetChanged();}else{Toast.makeText(this,"发生未知错误",Toast.LENGTH_SHORT).show();finish();}break;default:}}private class MyBroadcast extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent){showSongName.setText( intent.getStringExtra("SongName"));}}@Overrideprotected void onDestroy() {super.onDestroy();Intent i=new Intent(this,MusicPlayService.class);stopService(i);}}
注释1处的代码构建了活动和服务通信的ServiceConnection。下面的代码是在绑定服务和活动初始知是就利用musicServiceBinder对服务进行一些初始化,这里我传入的是歌曲的list。
public void onServiceConnected(ComponentName name, IBinder service) { musicServiceBinder=(MusicPlayService.MusicServiceBinder)service;musicServiceBinder.InitBinder(mSongList);}
下面这段代码我动态注册了一个广播用于服务去通知活动更新UI界面。
IntentFilter intentFilter=new IntentFilter();intentFilter.addAction("com.example.iMusic.UI_UPDATE");MyBroadcast my=new MyBroadcast();registerReceiver(my,intentFilter);
下面就是我的广播类或许你会奇怪我为什么就更新了音乐名字这一条。 因为你在这份主活动代码的初始部分可以看到的一段注释,我把play这个按钮给静态化了。这样就可以在服务这个类里直接使用了,但这里却为何不适用相同的方法去更改歌曲名呢?其实我只是想用两种方法,在一个项目里去练习多种不同的东西。就我个人来看,用staitc这种方法有点奇怪,广播的方法会比较正规。
private class MyBroadcast extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent){showSongName.setText( intent.getStringExtra("SongName"));}}
下面是上一曲,下一曲,播放按键的点击事件。这里我写的就比较简单了因为大部分工作是放到服务里去对mediaplayer进行操作了。 在代码初始的变量声明我说明了state是个关于播放状态的记录值。1表明现在处于播放状态 0表示处于暂停状态/未初始化状态。这里play的点击事件主要是对state变量的值更改,还有对相应情况下play按钮的背景进行更改。
last.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {musicServiceBinder.last();}});next.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {musicServiceBinder.next();}});play.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {if(state==1){musicServiceBinder.pause();play.setBackgroundResource(R.drawable.music_play);state=0;}else{play.setBackgroundResource(R.drawable.music_pause);musicServiceBinder.play();state=1;}}});
下面这段代码是侧拉菜单的一些配置,各位不熟悉的话可以自行百度学习下,也不难,不过说实话,我的侧拉菜单点开其实很慢这是需要改进的地方。我的定时活动入口(timing)就放在了侧拉菜单里了。
mDrawerLayout=(DrawerLayout)findViewById(R.id.drawer_layout);NavigationView navigationView=(NavigationView)findViewById(R.id.nav_view);navigationView.setCheckedItem(R.id.setting1);navigationView.setCheckedItem(R.id.timing);navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {@Overridepublic boolean onNavigationItemSelected(@NonNull MenuItem item) {switch (item.getItemId()){case R.id.setting1:mDrawerLayout.closeDrawers();break;case R.id.timing:Intent timingIntent=new Intent(MainActivity.this,timing.class);startActivity(timingIntent);}return true;}});
其余中间就是一些权限申请,和recyclerview的一些配置相信各位必然看的懂了 。下面这段代码是是对停止服务的运行,避免服务在活动关闭还一直运行。
protected void onDestroy() {super.onDestroy();Intent i=new Intent(this,MusicPlayService.class);stopService(i);}
哦,还要说下面这段代码这段代码在确认了已经获得访问存储卡权限,对歌曲list进行了更新。
if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED){permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);}else{mSongList=ScanMusicUtil.scanMusic(this);}
下面分析MusicService的代码,代码如下:
public class MusicPlayService extends Service {private List<Song> mSongList=new ArrayList<>();public static MediaPlayer mediaPlayer;public MusicServiceBinder musicServiceBinder=new MusicServiceBinder();private MainActivity activity;private int SongPointer=-1;private int i=0;class MusicServiceBinder extends Binder {public void InitMedia(Song music,int pointer){try {if(mediaPlayer.isPlaying()){mediaPlayer.stop();mediaPlayer.reset();}else{mediaPlayer.reset();}SongPointer=pointer;Intent intent=new Intent("com.example.iMusic.UI_UPDATE");intent.putExtra("SongName",music.getName());sendBroadcast(intent);mediaPlayer.setDataSource(music.getPath());mediaPlayer.prepare();}catch (Exception e){e.printStackTrace();}}public void InitBinder(List<Song> mSongList1){Init(mSongList1);}public void start(){mediaPlayer.start();}public void pause(){mediaPlayer.pause();}public void stop(){mediaPlayer.stop();}public void reset(){mediaPlayer.reset();}public void setDataSource(String url){try{mediaPlayer.setDataSource(url);}catch (IOException e){e.printStackTrace();}}public void prepare(){try{mediaPlayer.prepare();}catch (IOException e){e.printStackTrace();}}public void last(){if(SongPointer>0)SongPointer--;InitMedia(mSongList.get(SongPointer),SongPointer);mediaPlayer.start();}public void next(){if(SongPointer<mSongList.size()-1){SongPointer++;InitMedia(mSongList.get(SongPointer),SongPointer);mediaPlayer.start();}}public void play(){if(SongPointer==-1){InitMedia(mSongList.get(0),SongPointer);SongPointer=0;}try{mediaPlayer.start();}catch (Exception e){e.printStackTrace();}}}@Overridepublic IBinder onBind(Intent intent) {return new MusicServiceBinder();}@Overridepublic void onCreate() {super.onCreate();Log.d("xxx","123");mediaPlayer=new MediaPlayer();mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {@Overridepublic void onCompletion(MediaPlayer mp) {if(SongPointer<mSongList.size()-1){SongPointer++;musicServiceBinder.InitMedia(mSongList.get(SongPointer),SongPointer);mediaPlayer.start();Log.d("Completion","Song"+SongPointer+" "+mSongList.get(SongPointer).getName() );}}});}public MusicPlayService(MainActivity activity){this.activity=activity;}@Overridepublic int onStartCommand(Intent intent,int flags, int startId) {return super.onStartCommand(intent, flags, startId);}public void Init(List<Song> mSongList1){mSongList=mSongList1;}@Overridepublic void onDestroy() {super.onDestroy();mediaPlayer.release();}
}
服务里的代码稍微复杂一些,没有接触过服务的朋友可能会看不懂。我先稍微说下结构:
class MusicServiceBinder 这个是服务和活动的通信类一些需要在活动里调用的方法可以写在这里。
public IBinder onBind 这个起的是绑定作用返回一个IBinder在本项目里就是MusiceService的实例咯。
public void onCreate 这个和活动的onCreate类似做一些UI和控件的初始化。
public int onStartCommand 这个方法我这里没写啥,这个和onCreate的区别是onCreate只在服务的创建时调用,而这个方法每次启动都会调用。
public void Init(List<Song> mSongList1) 这个是初始化服务的歌曲列表的方法供MusicServiceBinder类调用。
public void onDestroy() 销毁方法这里我释放了MediaPlayer的资源。
首先我们分析下onCreate里的东西,从这里开始会比较简单。代码如下,可以看到这里我创建了MediaPlayer的实例。然后设置了它的CompletionListener。这个接口是干嘛的呢。顾名思义,就是在MediaPlayer完成一首音乐的播放时会调用这里。在这里我让歌曲的指针自增,然后接着用MusicServiceBinder的实例的InitMedia()初始化一些下一首音乐的资源。最后让MediaPlayer的实例重新start起来。
public void onCreate() {super.onCreate();mediaPlayer=new MediaPlayer();mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {@Overridepublic void onCompletion(MediaPlayer mp) {if(SongPointer<mSongList.size()-1){SongPointer++;musicServiceBinder.InitMedia(mSongList.get(SongPointer),SongPointer);mediaPlayer.start();}}});}
下面开始class MusicServiceBinder的讲解,这部分比较复杂需要耐心看。首先看下刚才我们的讲到的InitMedia()(如下代码注释1处).这里首先判断了mediaPlayer是否在运行,如果在运行的话我们就让他停掉,并且重置它,如果不在运行,就直接重置它。然后让SongPointer=参数里的pointer。接着就发出广播啦,提醒我们的主活动去更新UI界面了。 mediaPlayer.setDataSource设置资源的方法,prepare就让mediaPlayer处于准备状态。
InitBinder()方法是初始化MusiceService里的歌曲list供活动调用的,这里有调用MusicService类的Init()方法去更新MusicService的歌曲list.
然后剩下的一些方法就是一些对meidaPlayer的上一曲,下一曲的操作了,不赘述了。
class MusicServiceBinder extends Binder {public void InitMedia(Song music,int pointer){//1try {if(mediaPlayer.isPlaying()){mediaPlayer.stop();mediaPlayer.reset();}else{mediaPlayer.reset();}SongPointer=pointer;Intent intent=new Intent("com.example.iMusic.UI_UPDATE");intent.putExtra("SongName",music.getName());sendBroadcast(intent);mediaPlayer.setDataSource(music.getPath());mediaPlayer.prepare();}catch (Exception e){e.printStackTrace();}}public void InitBinder(List<Song> mSongList1){Init(mSongList1);}public void start(){mediaPlayer.start();}public void pause(){mediaPlayer.pause();}public void stop(){mediaPlayer.stop();}public void reset(){mediaPlayer.reset();}public void setDataSource(String url){try{mediaPlayer.setDataSource(url);}catch (IOException e){e.printStackTrace();}}public void prepare(){try{mediaPlayer.prepare();}catch (IOException e){e.printStackTrace();}}public void last(){if(SongPointer>0)SongPointer--;InitMedia(mSongList.get(SongPointer),SongPointer);mediaPlayer.start();}public void next(){if(SongPointer<mSongList.size()-1){SongPointer++;InitMedia(mSongList.get(SongPointer),SongPointer);mediaPlayer.start();}}public void play(){if(SongPointer==-1){InitMedia(mSongList.get(0),SongPointer);SongPointer=0;}try{mediaPlayer.start();}catch (Exception e){e.printStackTrace();}}}
下面是timing活动代码如下:
这里就是利用AlarmManager 的一个定时服务了。
public class timing extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_timing);ActionBar actionBar =getSupportActionBar();if(actionBar!=null){actionBar.hide();}final EditText timingText=(EditText) findViewById(R.id.timingText);Button finishButton=(Button)findViewById(R.id.timingFinishButton);ImageButton backImgButton=(ImageButton) findViewById(R.id.back);backImgButton.setOnClickListener(new View.OnClickListener(){@Overridepublic void onClick(View v) {finish();}});finishButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {long time=Long.parseLong(timingText.getText().toString());long triggerAtTime = SystemClock.elapsedRealtime()+time*1000;AlarmManager manager =(AlarmManager) getSystemService(Context.ALARM_SERVICE);Intent i=new Intent(timing.this,timingstopService.class);PendingIntent pi=PendingIntent.getService(timing.this,0,i,0);manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pi);}});
}
}
timingstopService的代码:
public class timingstopService extends Service {public timingstopService() {}@Overridepublic IBinder onBind(Intent intent) {// TODO: Return the communication channel to the service.throw new UnsupportedOperationException("Not yet implemented");}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {MusicPlayService.mediaPlayer.pause();MainActivity.state=0;MainActivity.play.setBackgroundResource(R.drawable.music_play);return super.onStartCommand(intent, flags, startId);}
}
songAdpater代码:
public class songAdpater extends RecyclerView.Adapter<songAdpater.ViewHolder> {private List<Song> mSongList;private MainActivity activity;static class ViewHolder extends RecyclerView.ViewHolder {TextView songName;TextView songAuthor;LinearLayout linearLayout;public ViewHolder(View v) {super(v);songName = (TextView) v.findViewById(R.id.song_name);songAuthor = (TextView) v.findViewById(R.id.song_author);linearLayout = (LinearLayout) v.findViewById(R.id.layout);}}public songAdpater(List<Song> mSongList, MainActivity activity) {this.mSongList = mSongList;this.activity = activity;}@Overridepublic ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.song_item, parent, false);final ViewHolder holder = new ViewHolder(view);holder.linearLayout.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {int position = holder.getAdapterPosition();Song song = mSongList.get(position);activity.play.setBackgroundResource(R.drawable.music_pause);activity.state = 1;activity.musicServiceBinder.InitMedia(song, position);activity.musicServiceBinder.start();}});return holder;}@Overridepublic void onBindViewHolder(ViewHolder holder, int position) {Song song = mSongList.get(position);if (song.getAuthor() != null)holder.songAuthor.setText(song.getAuthor());holder.songName.setText(song.getName());}@Overridepublic int getItemCount() {return mSongList.size();}}
Song类代码:
public class Song {private String name;private String author;private String path;public String getPath() {return path;}public void setPath(String path) {this.path = path;}public String getAuthor() {return author;}public void setAuthor(String author) {this.author = author;}public String getName() {return name;}public void setName(String name) {this.name = name;}
}
ScanMusicUtil类代码
public class ScanMusicUtil {public static ArrayList<Song> scanMusic(Context context){ArrayList<Song> musicList= new ArrayList<Song>();Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null,null, MediaStore.Audio.AudioColumns.IS_MUSIC);if(cursor!=null){while(cursor.moveToNext()){Song music=new Song();music.setName(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)));music.setPath(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)));// music.setCoverId(cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)));musicList.add(music);Log.d("testest",music.getName()+" "+music.getPath());}}cursor.close();return musicList;}
}
Android 简单音乐播放器开发相关推荐
- Android简单音乐播放器
话不多说先上效果 前言 写这个音乐播放器实在是迫不得已.因为我们Andoird课程要求写一个音乐播放器.所以就有了此项目.这个项目比较简单,实现了最基本的音乐播放功能,然后界面是仿照着网易云音乐的样式 ...
- android版音乐播放器开发教程,Android音乐播放器开发文档(20200907152026).pdf
Android 音乐播放器 撰写人:张 XX 2011-3-15 一. 问题定义 本软件是为了用户智能手机 Android 而开发的一套智能软件, 提供在线 下载音乐,在线播放音乐,读取 SD 卡音乐 ...
- android简单音乐播放器(二)
还是使用Activity方法播放音乐,因为我感觉还是没有弄懂service播放音乐,当然Activity播放,如果退出这个应用,音乐就不播放了- 上代码: MainActivity.java pack ...
- android音乐播放器实现,Android实现简单音乐播放器(MediaPlayer)
Android实现简单音乐播放器(MediaPlayer),供大家参考,具体内容如下 开发工具:Andorid Studio 1.3 运行环境:Android 4.4 KitKat 工程内容 实现一个 ...
- Android音乐播放器开发(6)—ListView组件创建歌曲播放列表(内含原理分析)
1. 说明 源码已同步到Gitee仓库,GitHub仓库,觉得还不错的话帮忙点个"star"吧,非常感谢. 以往的文章 服务端:Android音乐播放器开发–服务端 登录:Andr ...
- Android音乐播放器开发(5)—播放界面(播放、暂停、上一首、下一首,顺序播放、随机播放、拖拽进度条…)
1. 说明 源码已同步到Gitee仓库,Github仓库,觉得还不错的话帮忙点个"star"吧,非常感谢. Android播放器专栏其它文章: 服务端:Android音乐播放器开发 ...
- Android如何实现简单音乐播放器的代码
想必大家在学Android的音乐播放器时肯定会遇到很多困难,不怕,在这里我给大家分享一个很简单的音乐播放器,绝对会对你的学习有所帮助.本例子不难,但是也确能给你带来柳暗花明又一村的感觉.闲话少说,马上 ...
- Android复习02(ListView具体操作[很详细]、简单音乐播放器)
2020年 3月24日 星期二 Android录播回放 笔记[腾讯课堂] https://ke.qq.com/webcourse/index.html#cid=989760&term_id=1 ...
- java计算机毕业设计vue开发一个简单音乐播放器(附源码、数据库)
java计算机毕业设计vue开发一个简单音乐播放器(附源码.数据库) 项目运行 环境配置: Jdk1.8 + Tomcat8.5 + Mysql + HBuilderX(Webstorm也行)+ Ec ...
最新文章
- java继承详解加练习题
- Python基于MASK信息抽取ROI子图实战:原始影像和mask文件都是二维的情况
- 【c语言】蓝桥杯算法训练 大小写转换
- Intel汇编语言程序设计课后习题,6.5.5
- 利用Minhash和LSH寻找相似的集合
- MySQL 系统架构 说明
- Redis中查找大key
- centos7 时间设置
- python服务器搭建 实战_实战讲解:如何用Python搭建一个服务器
- Lucene 概念,定义应用场景
- Pytorch squeeze() 和 unsqueeze() 方法区别
- 目录遍历漏洞_雷神众测漏洞周报 2020.10.052020.10.114
- 记一次新旧系统数据迁移
- 北京航空航天大学计算机学院 赵,北京航空航天大学计算机学院计算机应用技术导师介绍:夏春和...
- NET Reflector
- 宿华卸任快手CEO程一笑接替;新思科技扩大与台积公司的战略技术合作 | 全球TMT...
- 复现Thinkphp5 5.0.22/5.1.29远程代码执行漏洞
- KubernetesDatabase-k8s中helm方式安装postgresql及pgadmin
- 小康qq小助手 v1.0 官网
- 最新简约虚拟资源下载站源码+织梦Dedecms内核
热门文章
- R语言数据可视化 画并列条形图和堆叠条形图
- 返回查找对象所在列标_返回基础-这不是您要查找的对象...等等,哦,它是对象...
- 使用Android Studio搭建Android源码查看工具
- 自定义组件中添加其他组件-1 83课 左边部分,右边部分的测试
- SpringBoot使用拦截器实现Restful URL权限拦截
- 基于css的表单模板
- 想做Python开发,这8种常用Python模块,你必须得知道!
- VM-tools选项为灰色无法安装的问题
- Flask学习笔记(四): Flask与数据库连接
- word如何一键全选_word文档怎么全选所有内容