V4L2学习(五)VIVI虚拟摄像头驱动
概述
前面简单分析了内核中虚拟摄像头驱动 vivi 的框架与实现,本文参考 vivi 来写一个虚拟摄像头驱动,查询、设置视频格式相对简单,难点在于 vb2_buf 的处理过程。
数据采集流程分析
在我的程序中,大概的数据采集流程如上图所示,启动视频采集之后,创建了一个内核线程,内核线程每30ms 唤醒一次,每一次唤醒都会尝试用 queue_list 中取出一个 buffer 填充数据之后挂入 done_list ,挂入 done_list 之后就会唤醒应用程序(poll 中休眠),应用程序唤醒之后就会 dqbuf 获取数据,处理完数据再 qbuf 把 buffer 挂入 queue_list 的头部,一直循环。
代码
- #include <linux/module.h>
- #include <linux/errno.h>
- #include <linux/kernel.h>
- #include <linux/init.h>
- #include <linux/sched.h>
- #include <linux/slab.h>
- #include <linux/font.h>
- #include <linux/mutex.h>
- #include <linux/videodev2.h>
- #include <linux/kthread.h>
- #include <linux/freezer.h>
- #include <media/videobuf2-vmalloc.h>
- #include <media/v4l2-device.h>
- #include <media/v4l2-ioctl.h>
- #include <media/v4l2-ctrls.h>
- #include <media/v4l2-fh.h>
- #include <media/v4l2-event.h>
- #include <media/v4l2-common.h>
- #define VFL_TYPE_GRABBER 0
- #define MAX_WIDTH 1920
- #define MAX_HEIGHT 1200
- static unsigned int vid_limit = ;
- static struct video_device *video_dev; // video_device 结构,用来描述一个 video 设备
- static struct vb2_queue vivi_queue; // vivi_queue 用来存放缓冲区信息,缓冲区链表等
- struct task_struct *kthread; // 内核线程,用来向缓冲区中填充数据
- DECLARE_WAIT_QUEUE_HEAD(wait_queue_head); // 等待队列头
- struct list_head my_list; // 链表头
- // 用来存放应用程序设置的视频格式
- static struct mformat {
- __u32 width;
- __u32 height;
- __u32 pixelsize;
- __u32 field;
- __u32 fourcc;
- __u32 depth;
- }mformat;
- static void mvideo_device_release(struct video_device *vdev)
- {
- }
- static long mvideo_ioctl(struct file *file, unsigned int cmd, void *arg)
- {
- int ret = ;
- struct v4l2_fh *fh = NULL;
- switch (cmd) {
- case VIDIOC_QUERYCAP:
- {
- struct v4l2_capability *cap = (struct v4l2_capability *)arg;
- cap->version = LINUX_VERSION_CODE;
- ret = video_dev->ioctl_ops->vidioc_querycap(file, NULL, cap);
- break;
- }
- case VIDIOC_ENUM_FMT:
- {
- struct v4l2_fmtdesc *f = arg;
- if (f->type == V4L2_BUF_TYPE_VIDEO_CAPTURE) {
- ret = video_dev->ioctl_ops->vidioc_enum_fmt_vid_cap(file, fh, f);
- }else{
- printk("V4L2_BUF_TYPE_VIDEO_CAPTURE error\n");
- }
- break;
- }
- case VIDIOC_G_FMT:
- {
- struct v4l2_format *f = (struct v4l2_format *)arg;
- if (f->type == V4L2_BUF_TYPE_VIDEO_CAPTURE) {
- ret = video_dev->ioctl_ops->vidioc_g_fmt_vid_cap(file, fh, f);
- }else{
- printk("V4L2_BUF_TYPE_VIDEO_CAPTURE error\n");
- }
- break;
- }
- case VIDIOC_TRY_FMT:
- {
- struct v4l2_format *f = (struct v4l2_format *)arg;
- if (f->type == V4L2_BUF_TYPE_VIDEO_CAPTURE) {
- ret = video_dev->ioctl_ops->vidioc_try_fmt_vid_cap(file, fh, f);
- }else{
- printk("V4L2_BUF_TYPE_VIDEO_CAPTURE error\n");
- }
- break;
- }
- case VIDIOC_S_FMT:
- {
- struct v4l2_format *f = (struct v4l2_format *)arg;
- if (f->type == V4L2_BUF_TYPE_VIDEO_CAPTURE) {
- video_dev->ioctl_ops->vidioc_s_fmt_vid_cap(file, fh, f);
- }else{
- printk("V4L2_BUF_TYPE_VIDEO_CAPTURE error\n");
- }
- break;
- }
- case VIDIOC_REQBUFS:
- {
- struct v4l2_requestbuffers *p = arg;
- ret = video_dev->ioctl_ops->vidioc_reqbufs(file, fh, p);
- break;
- }
- case VIDIOC_QUERYBUF:
- {
- struct v4l2_buffer *p = arg;
- ret = video_dev->ioctl_ops->vidioc_querybuf(file, fh, p);
- break;
- }
- case VIDIOC_QBUF:
- {
- struct v4l2_buffer *p = arg;
- ret = video_dev->ioctl_ops->vidioc_qbuf(file, fh, p);
- break;
- }
- case VIDIOC_DQBUF:
- {
- struct v4l2_buffer *p = arg;
- ret = video_dev->ioctl_ops->vidioc_dqbuf(file, fh, p);
- break;
- }
- case VIDIOC_STREAMON:
- {
- enum v4l2_buf_type i = *(int *)arg;
- ret = video_dev->ioctl_ops->vidioc_streamon(file, fh, i);
- break;
- }
- case VIDIOC_STREAMOFF:
- {
- enum v4l2_buf_type i = *(int *)arg;
- ret = video_dev->ioctl_ops->vidioc_streamoff(file, fh, i);
- break;
- }
- }
- return ret;
- }
- static int vivi_mmap(struct file *file, struct vm_area_struct *vma)
- {
- int ret;
- printk("enter mmap\n");
- ret = vb2_mmap(&vivi_queue, vma);
- if(ret == ){
- printk("mmap ok\n");
- }else{
- printk("mmap error\n");
- }
- return ret;
- }
- // 查询设备能力
- static int mvidioc_querycap(struct file *file, void *priv, struct v4l2_capability *cap)
- {
- strcpy(cap->driver, "vivi");
- strcpy(cap->card, "vivi");
- strcpy(cap->bus_info, "mvivi");
- cap->device_caps = V4L2_CAP_VIDEO_CAPTURE;
- cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS | V4L2_CAP_STREAMING;
- printk("mvidioc_querycap \n");
- return ;
- }
- // 枚举视频支持的格式
- static int vidioc_enum_fmt_vid_cap(struct file *file, void *priv, struct v4l2_fmtdesc *f)
- {
- printk("vidioc_enum_fmt_vid_cap \n");
- if (f->index >= )
- return -EINVAL;
- strcpy(f->description, "mvivi");
- f->pixelformat = mformat.fourcc;
- printk("vidioc_enum_fmt_vid_cap \n");
- return ;
- }
- // 修正应用层传入的视频格式
- static int vidioc_try_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f)
- {
- printk("vidioc_try_fmt_vid_cap\n");
- if (f->fmt.pix.pixelformat != V4L2_PIX_FMT_YUYV) {
- return -EINVAL;
- }
- f->fmt.pix.field = V4L2_FIELD_INTERLACED;
- v4l_bound_align_image(&f->fmt.pix.width, , MAX_WIDTH, ,
- &f->fmt.pix.height, , MAX_HEIGHT, , );
- f->fmt.pix.bytesperline = (f->fmt.pix.width * mformat.depth) / ;
- f->fmt.pix.sizeimage = f->fmt.pix.height * f->fmt.pix.bytesperline;
- if (mformat.fourcc == V4L2_PIX_FMT_YUYV)
- f->fmt.pix.colorspace = V4L2_COLORSPACE_SMPTE170M;
- else
- f->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
- return ;
- }
- // 获取支持的格式
- static int vidioc_g_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f)
- {
- // 将参数写回用户空间
- f->fmt.pix.width = mformat.width;
- f->fmt.pix.height = mformat.height;
- f->fmt.pix.field = mformat.field;
- f->fmt.pix.pixelformat = mformat.fourcc;
- f->fmt.pix.bytesperline = (f->fmt.pix.width * mformat.depth) / ;
- f->fmt.pix.sizeimage = f->fmt.pix.height * f->fmt.pix.bytesperline;
- if (mformat.fourcc == V4L2_PIX_FMT_YUYV)
- f->fmt.pix.colorspace = V4L2_COLORSPACE_SMPTE170M;
- else
- f->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
- printk("vidioc_g_fmt_vid_cap \n");
- return ;
- }
- // 设置视频格式
- static int vidioc_s_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f)
- {
- int ret = vidioc_try_fmt_vid_cap(file, priv, f);
- if (ret < )
- return ret;
- // 存储用户空间传入的参数设置
- mformat.fourcc = V4L2_PIX_FMT_YUYV;
- mformat.pixelsize = mformat.depth / ;
- mformat.width = f->fmt.pix.width;
- mformat.height = f->fmt.pix.height;
- mformat.field = f->fmt.pix.field;
- printk("vidioc_s_fmt_vid_capp \n");
- return ;
- }
- // vb2 核心层 vb2_reqbufs 中调用它,确定申请缓冲区的大小
- static int queue_setup(struct vb2_queue *vq, const struct v4l2_format *fmt,
- unsigned int *nbuffers, unsigned int *nplanes,
- unsigned int sizes[], void *alloc_ctxs[])
- {
- unsigned long size;
- printk("mformat.width %d \n",mformat.width);
- printk("mformat.height %d \n",mformat.height);
- printk("mformat.pixelsize %d \n",mformat.pixelsize);
- size = mformat.width * mformat.height * mformat.pixelsize;
- if ( == *nbuffers)
- *nbuffers = ;
- while (size * *nbuffers > vid_limit * * )
- (*nbuffers)--;
- *nplanes = ;
- sizes[] = size;
- return ;
- }
- static int buffer_init(struct vb2_buffer *vb)
- {
- return ;
- }
- static int buffer_finish(struct vb2_buffer *vb)
- {
- return ;
- }
- static int buffer_prepare(struct vb2_buffer *vb)
- {
- unsigned long size;
- size = mformat.width * mformat.height * mformat.pixelsize;
- vb2_plane_size(vb, );
- //vb2_set_plane_payload(&buf->vb, 0, size);
- return ;
- }
- static void buffer_queue(struct vb2_buffer *vb)
- {
- }
- // 内核线程中填充数据,效果是一个逐渐放大的圆形,视频大小为 640 * 480
- static void vivi_fillbuff(struct vb2_buffer *vb)
- {
- void *vbuf = NULL;
- unsigned char (*p)[mformat.width][mformat.pixelsize];
- unsigned int i,j;
- vbuf = vb2_plane_vaddr(vb, );
- static unsigned int t = ;
- p = vbuf;
- for(j = ; j < mformat.height; j++)
- for(i = ; i < mformat.width; i++){
- if((j - )*(j - ) + (i - )*(i - ) < (t * t)){
- *(*(*(p+j)+i)+) = (unsigned char)0xff;
- *(*(*(p+j)+i)+) = (unsigned char)0xff;
- }else{
- *(*(*(p+j)+i)+) = (unsigned char);
- *(*(*(p+j)+i)+) = (unsigned char);
- }
- }
- t++;
- printk("%d\n",t);
- if( t >= mformat.height/) t = ;
- }
- // 内核线程每一次唤醒调用它
- static void vivi_thread_tick(void)
- {
- struct vb2_buffer *buf = NULL;
- struct list_head *list;
- struct vb2_buffer *task;
- unsigned long flags;
- if (list_empty(&vivi_queue.queued_list)) {
- //printk("No active queue to serve\n");
- return;
- }
- // 注意我们这里取出之后就删除了,剩的重复工作,但是在 dqbuf 时,vb2_dqbuf 还会删除一次,我做的处理是在dqbuf之前将buf随便挂入一个链表
- buf = list_entry(vivi_queue.queued_list.next, struct vb2_buffer, queued_entry);
- list_del(&buf->queued_entry);
- /* 填充数据 */
- vivi_fillbuff(buf);
- printk("filled buffer %p\n", buf->planes[].mem_priv);
- // 它干两个工作,把buffer 挂入done_list 另一个唤醒应用层序,让它dqbuf
- vb2_buffer_done(buf, VB2_BUF_STATE_DONE);
- }
- #define WAKE_NUMERATOR 30
- #define WAKE_DENOMINATOR 1001
- #define BUFFER_TIMEOUT msecs_to_jiffies(500) /* 0.5 seconds */
- #define frames_to_ms(frames) \
- ((frames * WAKE_NUMERATOR * ) / WAKE_DENOMINATOR)
- static void vivi_sleep(void)
- {
- int timeout;
- DECLARE_WAITQUEUE(wait, current);
- add_wait_queue(&wait_queue_head, &wait);
- if (kthread_should_stop())
- goto stop_task;
- /* Calculate time to wake up */
- timeout = msecs_to_jiffies(frames_to_ms());
- vivi_thread_tick();
- schedule_timeout_interruptible(timeout);
- stop_task:
- remove_wait_queue(&wait_queue_head, &wait);
- try_to_freeze();
- }
- static int vivi_thread(void *data)
- {
- set_freezable();
- for (;;) {
- vivi_sleep();
- if (kthread_should_stop())
- break;
- }
- printk("thread: exit\n");
- return ;
- }
- static int vivi_start_generating(void)
- {
- kthread = kthread_run(vivi_thread, video_dev, video_dev->name);
- if (IS_ERR(kthread)) {
- printk("kthread_run error\n");
- return PTR_ERR(kthread);
- }
- /* Wakes thread */
- wake_up_interruptible(&wait_queue_head);
- return ;
- }
- static int start_streaming(struct vb2_queue *vq, unsigned int count)
- {
- vivi_start_generating();
- return ;
- }
- static int stop_streaming(struct vb2_queue *vq)
- {
- if (kthread) {
- kthread_stop(kthread);
- kthread = NULL;
- }
- /*
- while (!list_empty(&vivi_queue.queued_list)) {
- struct vb2_buffer *buf;
- buf = list_entry(vivi_queue.queued_list.next, struct vb2_buffer, queued_entry);
- list_del(&buf->queued_entry);
- vb2_buffer_done(buf, VB2_BUF_STATE_ERROR);
- }
- */
- return ;
- }
- static struct vb2_ops vivi_video_qops = {
- .queue_setup = queue_setup,
- .buf_init = buffer_init,
- .buf_finish = buffer_finish,
- .buf_prepare = buffer_prepare,
- .buf_queue = buffer_queue,
- .start_streaming = start_streaming,
- .stop_streaming = stop_streaming,
- };
- static int mvivi_open(struct file *filp)
- {
- vivi_queue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
- vivi_queue.io_modes = VB2_MMAP;
- vivi_queue.drv_priv = video_dev;
- //vivi_queue.buf_struct_size = sizeof(struct vivi_buffer);
- vivi_queue.ops = &vivi_video_qops;
- vivi_queue.mem_ops = &vb2_vmalloc_memops;
- vivi_queue.name = "vb2";
- vivi_queue.buf_struct_size = sizeof(struct vb2_buffer);
- INIT_LIST_HEAD(&vivi_queue.queued_list);
- INIT_LIST_HEAD(&vivi_queue.done_list);
- spin_lock_init(&vivi_queue.done_lock);
- init_waitqueue_head(&vivi_queue.done_wq);
- mformat.fourcc = V4L2_PIX_FMT_YUYV;
- mformat.depth = ;
- INIT_LIST_HEAD(&my_list);
- return ;
- }
- static int vidioc_reqbufs(struct file *file, void *priv, struct v4l2_requestbuffers *p)
- {
- printk("vidioc_reqbufs \n");
- printk("count %d\n",p->count);
- printk("memory %d\n",p->memory);
- return vb2_reqbufs(&vivi_queue, p);
- }
- static int vidioc_querybuf(struct file *file, void *priv, struct v4l2_buffer *p)
- {
- printk("vidioc_querybuf \n");
- return vb2_querybuf(&vivi_queue, p);
- }
- static int vidioc_qbuf(struct file *file, void *priv, struct v4l2_buffer *p)
- {
- printk("vidioc_qbuf buffer \n");
- return vb2_qbuf(&vivi_queue, p);
- }
- static int vidioc_dqbuf(struct file *file, void *priv, struct v4l2_buffer *p)
- {
- printk("vidioc_dqbuf buffer \n");
- struct vb2_buffer *vb;
- vb = list_first_entry(&vivi_queue.done_list, struct vb2_buffer, done_entry);
- list_add_tail(&vb->queued_entry, &my_list);
- return vb2_dqbuf(&vivi_queue, p, file->f_flags & O_NONBLOCK);
- }
- static int vidioc_streamon(struct file *file, void *priv, enum v4l2_buf_type i)
- {
- printk("vidioc_streamon \n");
- return vb2_streamon(&vivi_queue, i);
- }
- static int vidioc_streamoff(struct file *file, void *priv, enum v4l2_buf_type i)
- {
- printk("vidioc_streamoff \n");
- return vb2_streamoff(&vivi_queue, i);
- }
- static struct v4l2_ioctl_ops mvivi_ioctl_ops = {
- .vidioc_querycap = mvidioc_querycap,
- .vidioc_enum_fmt_vid_cap = vidioc_enum_fmt_vid_cap,
- .vidioc_g_fmt_vid_cap = vidioc_g_fmt_vid_cap,
- .vidioc_try_fmt_vid_cap = vidioc_try_fmt_vid_cap,
- .vidioc_s_fmt_vid_cap = vidioc_s_fmt_vid_cap,
- .vidioc_reqbufs = vidioc_reqbufs,
- .vidioc_querybuf = vidioc_querybuf,
- .vidioc_qbuf = vidioc_qbuf,
- .vidioc_dqbuf = vidioc_dqbuf,
- .vidioc_streamon = vidioc_streamon,
- .vidioc_streamoff = vidioc_streamoff,
- };
- static unsigned int mvivi_poll(struct file *file, struct poll_table_struct *wait)
- {
- struct vb2_buffer *vb = NULL;
- int res = ;
- printk("enter the poll \n");
- poll_wait(file, &vivi_queue.done_wq, wait);
- if (!list_empty(&vivi_queue.done_list))
- vb = list_first_entry(&vivi_queue.done_list, struct vb2_buffer, done_entry);
- if (vb && (vb->state == VB2_BUF_STATE_DONE || vb->state == VB2_BUF_STATE_ERROR)) {
- return (V4L2_TYPE_IS_OUTPUT(vivi_queue.type)) ?
- res | POLLOUT | POLLWRNORM :
- res | POLLIN | POLLRDNORM;
- }
- return ;
- }
- long video_ioctl2(struct file *file, unsigned int cmd, unsigned long arg)
- {
- return video_usercopy(file, cmd, arg, mvideo_ioctl);
- }
- static struct v4l2_file_operations mvivi_fops = {
- .owner = THIS_MODULE,
- .open = mvivi_open,
- .unlocked_ioctl = video_ioctl2,
- //.release = mvivi_close,
- .poll = mvivi_poll,
- .mmap = vivi_mmap,
- };
- static struct video_device vivi_template = {
- .name = "mvivi",
- .fops = &mvivi_fops,
- .ioctl_ops = &mvivi_ioctl_ops,
- .release = mvideo_device_release,
- };
- static int mvivi_init(void)
- {
- int ret;
- video_dev = video_device_alloc();
- *video_dev = vivi_template;
- ret = video_register_device(video_dev, VFL_TYPE_GRABBER, -);
- if(ret != ){
- printk(" video_register_device error\n");
- }else{
- printk(" video_register_device ok\n");
- }
- return ret;
- }
- static void mvivi_exit(void)
- {
- video_unregister_device(video_dev);
- }
- module_init(mvivi_init);
- module_exit(mvivi_exit);
- MODULE_LICENSE("GPL");
V4L2学习(五)VIVI虚拟摄像头驱动的更多相关文章
- V4L2(二)虚拟摄像头驱动vivi深入分析【转】
转自:http://www.cnblogs.com/tureno/articles/6694463.html 转载于: http://blog.csdn.net/lizuobin2/article/d ...
- vivi虚拟摄像头驱动程序
一.vivi虚拟摄像头驱动 基于V4L2(video for linux 2)摄像头驱动程序,我们减去不需要的ioctl_fops的函数,只增加ioctl函数增加的必要的摄像头流查询等函数: #inc ...
- 二十四、V4L2框架主要结构体分析和虚拟摄像头驱动编写
一.V4L2框架主要结构体分析 V4L2(video for linux version 2),是内核中视频设备的驱动框架,为上层访问视频设备提供统一接口. V4L2整体框架如下图: 图中主要包括两层 ...
- 通过虚拟驱动vivi分析摄像头驱动
Linux摄像头驱动学习之:(二)通过虚拟驱动vivi分析摄像头驱动 一.通过指令 "strace -o xawtv.log xawtv" 得到以下调用信息: // 1~7都是在v ...
- 2.2 vivi虚拟视频驱动测试
学习目标:在linux终端安装xawtv,并测试vivi.ko驱动程序. 一.安装xawtv 1)ubuntu能上网情况下,使用命令:# sudo apt-get install xawtv 2)如果 ...
- Linux摄像头驱动学习之:(二)通过虚拟驱动vivi分析摄像头驱动
一.通过指令 "strace -o xawtv.log xawtv" 得到以下调用信息:// 1~7都是在v4l2_open里调用1. open2. ioctl(4, VIDIOC ...
- (一)V4L2学习流程
title: V4L2学习流程 date: 2019/4/23 18:00:00 toc: true --- V4L2学习流程 参考资料 关键资料,插图让人一下子就理解了 Linux摄像头驱动1--v ...
- V4l2初识(七)-----------浅析app获取虚拟摄像头数据的过程
继续分析数据的获取过程: 1.请求分配的缓冲区: ioctl(4,VIDIOC_REQBUFS) vidioc_reqbufs 2.查询和映射缓冲区 ioctl(4,VIDIOC_QUERYBUF ...
- 2.3 摄像头驱动_vivi驱动程序分析
学习目标:熟悉vivi的调用过程,分析vivi程序源码的ioctl函数: 一.vivi虚拟视频驱动测试方法 当我们接上usb摄像头设备时,系统会自动给我们安装对应的usb设备驱动程序.如果下次直接测试 ...
随机推荐
- .net core mvc项目部署nginx报错一直显示404错误
遇到一个奇怪的问题,.net core mvc 项目部署到nginx上面,系统是linux,controller明明抛出500错误,但页面一直显示是404. 解决如下: 1.修改Startup.cs, ...
- cf314E. Sereja and Squares(dp)
题意 题目链接 给你一个擦去了部分左括号和全部右括号的括号序列,括号有25种,用除x之外的小写字母a~z表示.求有多少种合法的括号序列.答案对4294967296取模.合法序列不能相交,如()[],( ...
- Vue通过状态为页面切换添加loading、为ajax加载添加loading
以下方法需要引入vuex,另使用了vux的UI框架,ajax添加loading还引入了axios. 一.为页面切换添加loading. loading.js: import Vue from 'vue ...
- thinkphp实现简易签到
老司机们,没时间了,直接贴代码: 视图: <!DOCTYPE html><html><meta charset="utf-8" /><ti ...
- Invoke 和 BeginInvoke 的区别(转发)
在Invoke或者BeginInvoke的使用中无一例外地使用了委托Delegate. 一.为什么Control类提供了Invoke和BeginInvoke机制? 关于这个问题的最主要的原因已经是do ...
- log4j-初识
1.配置文件介绍: 1.1. 控制台输出:log4j.rootLogger=DEBUG, Console ,File #Console log4j.appender.Console=org.apach ...
- Mac终端下使用***
首先安装proxychains: brew install proxychains-ng 然后创建文件~/.proxychains/proxychains.conf,写入以下内容: strict_ch ...
- ehcache常用API整理
鉴于csdn的blog的不稳定, 及混乱的编辑器, 和无上传功能, 遂决定彻底投诚javaeye的blog. 数月前整理的一个东西, 作为cache的扫盲文档.参考了它的官方文档. 对ehcache感 ...
- 在Spark集群中,集群的节点个数、RDD分区个数、cpu内核个数三者与并行度的关系
梳理一下Spark中关于并发度涉及的几个概念File,Block,Split,Task,Partition,RDD以及节点数.Executor数.core数目的关系. 输入可能以多个文件的形式存储在H ...
- 使用OpenFileDialog组件打开对话框
实现效果: 知识运用: OpenFileDialog组件的ShowDialog方法 public DialogResult Show () //返回枚举值 DialogRrsult.OK 或 Di ...