Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
RT-Thread一般讨论
关于控件和剪切域1
发布于 2011-01-14 23:49:57 浏览:6025
订阅该版
关于GUI中的剪切域 剪切域,是每个学习GUI的人必然要面对的一个技术问题。虽然不使用剪切域,也可以绘制出很漂亮的GUI界面,也可以处理多个窗口的Z序剪切。但是,如果想设计一款高效的GUI系统,避免不了要处理GUI的剪切域。 剪切域的意思是在绘制控件某块区域时,并不会影响到不相关的区域。如下图所示: ![clip-1.PNG](/uploads/1044_c8147b759e889bdd03357b4a0ca3b141.png) 上图中,在XP系统的桌面上点击一个图标时,不会显示被窗口覆盖的部分。那么,这个图标的剪切域就是图标的整个绘图区域去掉被窗口覆盖的部分。 再看一个实际的例子,下图 ![clip-2.PNG](/uploads/1044_2c0be23482b0811c7f75a4eaf8360f2e.png) 则三个窗口的剪切区域如下所示:(不包含紫色部分) ![clip-3.PNG](/uploads/1044_2a25aff2772ecfcf7a73465d487a7e80.png) ![clip-4.PNG](/uploads/1044_103d54bf584900c328ea2ba21c2dbeea.png) ![clip-5.PNG](/uploads/1044_6e23e0e0978c1b3e5809d67015c8b940.png) 这三张图片只是简单的反映了每个窗口的可绘制范围,实际上,它们的剪切域还需要剪切的所有可见的子控件的区域。 下面的图A演示了一个Label控件,这是RTGUI中哦一个例程。其中上面还有一个窗口 ![clip-a.PNG](/uploads/1044_bf8bea89c1761966af1205f83dc67ac8.png) 控件view的剪切域则是图B的样子: ![clip-b.PNG](/uploads/1044_300423d73a019db8927d190d0097f6f7.png) 为了便于观看,图B被放大了2倍。从图中可以看出,将覆盖在view控件之上的窗口和它的子控件区域都剪切掉之后,剩下一些矩形区域,图中蓝色的矩形框。实际上RTGUI中控件的剪切域信息就记录在一个矩形线性表当中。如果控件没有被其它控件覆盖,则控件的剪切域和控件的extent属性相同。 在RTGUI中,是如何处理剪切域的呢。首先,看一下,存储剪切域的数据结构。RTGUI中使用一下数据结构记录一个控件的剪切域信息, ```typedef struct rtgui_region_data rtgui_region_data_t; struct rtgui_region_data { rt_uint32_t size; rt_uint32_t numRects; /* XXX: And why, exactly, do we have this bogus struct definition? */ /* rtgui_rect_t rects; in memory but not explicitly declared */ }; typedef struct rtgui_region { rtgui_rect_t extents; rtgui_region_data_t *data; }rtgui_region_t;``` rtgui_rtgion_t这个类型就是RTGUI中用于记录剪切域的数据类型。在rtgui_widget_t这个类型中定义了一个成员变量rtgui_region_t clip;而所有的RTGUI控件都是从rtgui_widget_t这个类型派生出来的。那么所有的控件就可以使用clip这个变量记录自身的剪切信息了。关于剪切域的操作都在region.c和region.h两个文件中。这两个文件与GTK+和miniGUI等使用的region文件大同小异,都是用于操作剪切域。从上面的rtgui_region_t这个类型的定义可以看出,当剪切域是整个控件的可绘区域时,使用extents记录该可绘区域。当剪切域有多个矩形区域构成时,则这些信息记录到*data下。虽然rtgui_region_data_t结构体下只定义了两个成员变量,但是在运行态下,如果numRects>1时,在内存中该结构体后面会紧跟着存储着每个Rect的信息。使用这些Rect的方法是: rtgui_rect_t* prect; prect = ((rtgui_rect_t *)(owner->clip.data + index + 1)); 上式中index表示要使用的Rect的编号。 下面我们看看,RTGUI是如何设置一个控件的剪切域的。以创建一个按钮控件为例。RTGUI控件调用rtgui_button_t* rtgui_button_create(const char* text)函数在内存中创建一个按钮控件实体。然后调用函数rtgui_widget_set_rect(RTGUI_WIDGET(button), &rect)设置按钮的extent属性。在RTGUI的所有控件在创建实体后都要设置它们的extent属性,这个属性即是该控件的原始绘图区域,也是用于产生控件剪切域的源数据。设置完extent属性后,会调用函数void rtgui_container_add_child(rtgui_container_t *container, rtgui_widget_t* child),这个函数会将按钮添加到一个容器控件下,作为它的一个子控件。在这个函数中又调用了函数_rtgui_container_update_toplevel(rtgui_container_t* container);该函数定义如下: ```static void _rtgui_container_update_toplevel(rtgui_container_t* container) { struct rtgui_list_node* node; rtgui_list_foreach(node, &(container->children)) { rtgui_widget_t* child = rtgui_list_entry(node, rtgui_widget_t, sibling); /* set child toplevel */ child->toplevel = rtgui_widget_get_toplevel(RTGUI_WIDGET(container)); if (RTGUI_IS_CONTAINER(child)) { _rtgui_container_update_toplevel(RTGUI_CONTAINER(child)); } } }``` 这个函数的意思是,取得容器下的每个控件的顶端控件,然后更新这些顶端控件。例如,如果button在一个win中,那么这个button的toplevel控件就是win。读者可以自行去分析rtgui_widget_get_toplevel()函数是如何确认一个控件的toplevel控件的,在widget.c文件中。这里就不分析了。通过上面的步骤之后,RTGUI已经配置好了一个按钮控件,并明确了控件之间的上下级关系。这一点很重要,因为当更新一个控件的剪切域时,也需要根据控件之间的关系更新一些兄弟控件和父控件的剪切域。 上面的代码只是明面上看到的。实际上bernard设计RTGUI时,使用C语言封装了一些类似于C++语言的“类”。所以在执行上面的代码时,会通过一些钩子去执行实际的创造工作,实际上在调用rtgui_button_create()函数时已经开始了这些工作。从rtgui_button_t的类型定义就可以看出,rtgui_button_t是从rtgui_label_t类型派生来的,而rtgui_label_t类型又派生自rtgui_widget_t类。rtgui_widget_t是所有的RTGUI控件的基本类型。但是它不是最底层的,它的下面还有struct rtgui_object,之下还有一个类型rtgui_type_t,我们来看看关于rtgui_type_t这个类型是怎么定义的: ```/* rtgui type structure */ struct rtgui_type { /* type name */ char* name; /* hierarchy and depth */ struct rtgui_type **hierarchy; int hierarchy_depth; /* constructor and destructor */ rtgui_constructor_t constructor; rtgui_destructor_t destructor; /* size of type */ int size; };``` 没错,我们看到rtgui_type_t类型中定义了两个类似于C++机制中的构造函数和析构函数。相信看到这里的朋友,只要你了解一点C++机制的运行原理,就能明白一个大概的情形,就是只要是rtgui_type_t型的控件在constr时都会去执行它的constructor过程,同样destroy时都会去执行它的destructor过程。我们再看看rtgui_button_t类型的一个用于类型转换的宏定义 /** Gets the type of a button */ #define RTGUI_BUTTON_TYPE (rtgui_button_type_get()) 接着看rtgui_button_type_get()的定义: ```rtgui_type_t *rtgui_button_type_get(void) { static rtgui_type_t *button_type = RT_NULL; if (!button_type) { button_type = rtgui_type_create("button", RTGUI_LABEL_TYPE, sizeof(rtgui_button_t), RTGUI_CONSTRUCTOR(_rtgui_button_constructor), RTGUI_DESTRUCTOR(_rtgui_button_destructor)); } return button_type; }``` 看到这里,我们应该知道RTGUI是怎么在内存中创建出来一个控件的实例了吧。前面讲过,rtgui_type_t类型创建和销毁时会分别去执行它的构造和析构函数。RTGUI的所有控件都是使用这种机制来实现类型转换的。前面也提到过,所有的RTGUI控件都派生自rtgui_widget_t这个类型,而rtgui_widget_t最源头的就是rtgui_type_t,所以用这种方法来实现控件的类型转换是很方便的。接着以创建button为例进行说明。创建button时会首先在内存中开辟一个sizeof(rtgui_button_t)大小的控件来存放button控件,而构建它的原型是RTGUI_LABEL_TYPE,所有又转到rtgui_label_t的构造函数中,填补在button这个控件的内存空间中关于label的信息,。。。依此推导下去,会进入rtgui_widget_t控件的构造函数中。这个函数中,RTGUI会初始化一下剪切域 ```static void _rtgui_widget_constructor(rtgui_widget_t *widget) { if (!widget) return; /* set default flag */ widget->flag = RTGUI_WIDGET_FLAG_DEFAULT; /* init list */ rtgui_list_init(&(widget->sibling)); /* ...中间省略了一大段代码... */ /* init user data private to 0 */ widget->user_data = 0; /* init clip information */ rtgui_region_init(&(widget->clip)); /* init hardware dc */ rtgui_dc_client_init(widget); }``` rtgui_region_init()的工作其实就是将控件的extent属性赋值为空。到这里,线索似乎中断了,不知道下一步需要做什么。但是细心地读者应该发现,结合上面讲到的,在创建了控件之后,都会将控件挂载一个容器控件下作为它的一个子控件。在这之前会要求先设置控件的extent属性。也就是调用了下面这个函数: ```void rtgui_widget_set_rect(rtgui_widget_t* widget, const rtgui_rect_t* rect) { if (widget == RT_NULL || rect == RT_NULL) return; widget->extent = *rect; #ifndef RTGUI_USING_SMALL_SIZE /* reset mini width and height */ widget->mini_width = rtgui_rect_width(widget->extent); widget->mini_height = rtgui_rect_height(widget->extent); #endif /* it's not empty, fini it */ if (rtgui_region_not_empty(&(widget->clip))) { rtgui_region_fini(&(widget->clip)); } /* reset clip info */ rtgui_region_init_with_extents(&(widget->clip), rect); if ((widget->parent != RT_NULL) && (widget->toplevel != RT_NULL)) { /* update widget clip */ rtgui_widget_update_clip(widget->parent); } }``` 从这个函数中可以看出,在设置了控件的extent属性之后,接着初始化了控件的clip。其中,当判断clip为非空时,会先free掉。这也是前面讲述了在内存中创建了一个控件的实例之后,要将控件的clip置空的原因。对于新建的控件来说,它的clip可能是空的,但是也可能不是。最重要的是,当改变一个控件的大小时,如果调用了rtgui_widget_set_rect()函数,又没有对clip查空,会发生什么后果呢。 语句rtgui_region_init_with_extents(&(widget->clip), rect);是将控件的clip的extents设置为与控件的extent一致。对于多数新建的控件来说,如果没有与其它控件重叠的话。它的剪切域就是它的extent表示的区域。但是当该控件的toplevel控件与其它toplevel控件重叠时,该控件有可能也会被其它的toplevel控件覆盖住一部分或全部。这时就要根据情况更新该控件的clip信息。这个工作由语句rtgui_widget_update_clip(widget->parent)来完成。我们转到这个函数中看看它到底做了什么, ```/* * This function updates the clip info of widget */ void rtgui_widget_update_clip(rtgui_widget_t* widget) { struct rtgui_list_node* node; rtgui_widget_t *parent; /* no widget or widget is hide, no update clip */ if (widget == RT_NULL || RTGUI_WIDGET_IS_HIDE(widget)) return; parent = widget->parent; /* if there is no parent, do not update clip (please use toplevel widget API) */ if (parent == RT_NULL) { if (RTGUI_IS_TOPLEVEL(widget)) { /* if it's toplevel widget, update it by toplevel function */ rtgui_toplevel_update_clip(RTGUI_TOPLEVEL(widget)); } return; } /* reset clip to extent */ rtgui_region_reset(&(widget->clip), &(widget->extent)); /* limit widget extent in parent extent */ rtgui_region_intersect(&(widget->clip), &(widget->clip), &(parent->clip)); /* get the no transparent parent */ while(parent != RT_NULL && parent->flag & RTGUI_WIDGET_FLAG_TRANSPARENT) { parent = parent->parent; } if (parent != RT_NULL) { /* subtract widget clip in parent clip */ if (!(widget->flag & RTGUI_WIDGET_FLAG_TRANSPARENT)) { rtgui_region_subtract_rect(&(parent->clip), &(parent->clip), &(widget->extent)); } } /* * note: since the layout widget introduction, the sibling widget will not * intersect. */ /* if it's a container object, update the clip info of children */ if (RTGUI_IS_CONTAINER(widget)) { rtgui_widget_t* child; rtgui_list_foreach(node, &(RTGUI_CONTAINER(widget)->children)) { child = rtgui_list_entry(node, rtgui_widget_t, sibling); rtgui_widget_update_clip(child); } } }``` 从函数中分析,可以看出,首先判断这个控件有没有父控件,如果没有,则作为toplevel控件处理,更新toplevel控件的clip使用了单独的过程(在函数rtgui_toplevel_update_clip()中),因为一个GUI系统中可能存在多个toplevel控件,那么它们之间就可能存在相互覆盖的区域,所以要考虑到许多情况,这个情况放在后面介绍。 语句rtgui_region_intersect(&(widget->clip), &(widget->clip), &(parent->clip))保证控件不会超出到父控件的区域之外。当判断控件为是一个toplevel控件下的某一级子控件时,首先将控件的clip复位成控件的extent,即首先设定它是没有被覆盖的。然后用rtgui_region_subtract_rect()函数从该控件的父控件的clip中去除该控件(的extent区域),控件一般是显示在父控件区域的某个区域,所以父控件的clip要更新。同时会跳过透明的控件,这个不难理解。最后当判定该控件是一个容器控件时,需要更新下它下面的所有子控件的clip,这里使用了递归方法,对于内存较小的场合,就要控制好控件嵌套的层数。 前面讲到,当判定控件是一个toplevel控件时,会转入一个独立的过程,下面我们看看它是怎么处理的。相关代码在toplevel.c文件中,这里就不列举出来了。分析该函数,可以知道,它首先将toplevel的clip限制在屏幕区域之内。我们都知道,对于一个像窗口win这样的控件,如果它支持移动操作的话,是可以将窗口的一部分区域移动到屏幕之外的,而对于屏幕之外的绘图是不被允许的,所有这里要作这个区域限定。后面的代码与更新widget的clip原理是一样的,但是因为需要操作多个toplevel控件,所有引入了external_clip_rect,external_clip_size这两个参数,它们记录的就是当前已经显示的toplevel控件的区域信息,有了这些信息,就可以使用更新widget控件clip的方法,更新toplevel控件下的所有子控件的clip。 下面,讲讲RTGUI怎么使用控件的clip来绘制控件的。 在RTGUI中,一般使用DC的方式进行绘图。在绘图前,会先创建一个绘图DC。创建DC后,则在控件的剪切域内进行绘图。如果抛开剪切域的话,该绘图操作应该可以绘制出一个完成的控件来。如果只在控件的剪切域内绘图,则绘制时,加入了一个判断,当判断预绘制的点在剪切域内时则绘制该点,否则不进行绘制。这是不使用clip缓存的情况,这种情况下绘图过程加入了很多判断,效率有所下降,但是优点是内存开销小。另一种使用clip缓冲的情况,在更新clip信息时,会同时将每个rect的信息记录到一个缓存中,当绘图时,根据clip的信息,直接将clip区域贴图出来即可。这种方式优点是可以得到很高的绘图效率,缺点是会消耗大量的内存。具体使用哪种clip策略,用户可以根据实际情况而定。RTGUI只支持第一种方式。实际上,在CPU主频大约400MHZ的情况下,使用第一种clip策略并没有导致绘图效率明显的下降。 RTGUI在申请成功DC绘制指针之后,会获得一个绘图场景,这个绘图场景可以是一个控件的extent区域,也可以是一块内存空间。下面看一下绘置点函数的具体实现代码。 ```/** draw a logic point on device */ static void rtgui_dc_client_draw_point(struct rtgui_dc* self, int x, int y) { rtgui_rect_t rect; rtgui_widget_t *owner; if (self == RT_NULL) return; /* get owner */ owner = RTGUI_CONTAINER_OF(self, struct rtgui_widget, dc_type); if (!RTGUI_WIDGET_IS_DC_VISIBLE(owner)) return; x = x + owner->extent.x1; y = y + owner->extent.y1; if (rtgui_region_contains_point(&(owner->clip), x, y, &rect) == RT_EOK) { /* draw this point */ hw_driver->set_pixel(&(owner->gc.foreground), x, y); } }``` rtgui_region_contains_point()函数会判定要绘制的点是否在该控件的剪切域内,是则绘制。绘制横线竖线的函数的原理相同,这里不再赘述。 关于操作剪切域的很多函数,都在region.c这个文件中,感兴趣的朋友可以自己去分析,本文仅在讲述一个分析RTGUI操作控件剪切域的线索,希望能帮到一些对剪切域感兴趣的朋友。 续:以前不会添加图片,现在会添加了,所以修改了一下。 调试GUI程序是一件很枯燥的事情,随着了解的更深入,也总结了一些调试GUI程序的经验。调试GUI程序一定是要上实际开发板进行验证的。最多数情况下,用模拟器来运行GUI代码,也会得到一致的绘制结果。但是,模拟器并不能模拟出实际的延时效果,也模拟不了物理绘制的闪烁效果。所以还是要到物理平台上进行验证。 在调试剪切域时,尤其重视硬件平台的测试效果。如何验证某个控件的剪切域是否正确呢?当需要验证某个控件的剪切域时,可以采用下面的方法,这个方法很理想。 首先我们需要修改下底层的硬件绘图函数,例如修改rt_hw_lcd_draw_hline()函数。 ```rt_bool_t debug_gui_delay=0; void rt_hw_lcd_draw_hline(rtgui_color_t *c, rt_int32_t x1, rt_int32_t x2, rt_int32_t y) { rt_uint32_t idx; rt_uint16_t color; if(debug_gui_delay) {/* 这里增加了一段延时 */ rt_uint32_t i=100000; while(i--); } /* get color pixel */ color = rtgui_color_to_565(*c); for(idx = x1; idx < x2; idx ++) { _rt_framebuffer[y][idx] = color; } }``` debug_gui_delay是个全局变量,我们在需要观察绘图效果的代码前将它置1,在需要观察的代码后再将它置0;因为GUI绘图时只会在控件的剪切域内绘图,所以这样处理之后,我们就可以观察到剪切域是否设置正确了。 GUI程序很在意绘图时的闪烁现象。当某个控件只改动了局部时,我们应该使用局部重绘的方法。例如两个窗口有一部分相互重叠了,当下面的窗口被提升到上面时,可以只重绘重叠区域。 ![clip-6.PNG](/uploads/1044_0e6209f9fcadb45f7cbbd9df654d3e06.png)![clip-7.PNG](/uploads/1044_b0e2d9aed361d13c5e591be00e503b74.png) 例如图6图7所示,当点击窗口“DIR1”之后,窗口DIR1被提升到窗口Z序的最上面,这时可以将窗口DIR1的剪切域设置为两个窗口的重叠部分,那么在调用了ondraw事件之后只有重叠区域被绘制出来。同理,重绘窗口DIR1的标题栏,就可以变成活动窗口了。这样处理会大大降低绘图时的闪烁现象。 当有很多个窗口相互重叠时,也可以使用类似方法。 ![clip-8.PNG](/uploads/1044_e97a2bfe2cdaa5f4647d24234545a4db.png) 如果我们在上面图8中点击了窗口“win 10”,应该怎么处理,才能让重绘区域最小呢。仔细分析后会发现,在提升窗口10的过程中,对窗口2~窗口9没有任何影响。可以从这上面做文章。再分析一下,又发现提升窗口10有可能对窗口11~窗口15的任何一个造成影响,即有可能覆盖它们。了解了这些情况就好办了。可以这样做。在提升窗口10时,把窗口10的剪切域恢复成它的extent。然后计算窗口10和窗口11~窗口15的重叠区域。用重叠区域作初始化它们的剪切域,并剪切掉Z序在其之上的窗口。例如用窗口10和窗口14的重叠区域初始化窗口14的剪切域,并剪切掉窗口15和窗口10的区域(窗口15在窗口14之上,因为提升了窗口10,窗口10成了最上面的窗口)。如此处理之后,每个窗口实际上重绘的区域基本上很少,所以基本上看不到闪烁。 当移除一个窗口时,剪切域的操作又不太一样。因为移除了一个窗口,会露出下面与之重叠且Z序在其之下的窗口。如果是用鼠标点击“关闭”按钮的话,那么首先有一个提升被点击窗口的过程,这个过程与上面的情况相同。那么此时所有其他被显示的窗口的Z序都在该窗口之下(窗口被提升后会移到Z序的最上面)。这时候其他窗口都需要做相应的剪切。那么可以将被移除哦窗口与其他窗口的重叠区域作为重叠窗口的剪切域,并剪切掉Z序在重叠窗口之上的窗口。移除了一个窗口之后还可能露出“桌面”上的东西。所有还要重绘桌面与被移除窗口的重叠区域。“桌面”相当于一个Z序在最底层的窗口,它的剪切域需要剪切掉所有在它之上的窗口、控件。
查看更多
6
个回答
默认排序
按发布时间排序
bernard
2011-01-15
这家伙很懒,什么也没写!
论坛上确实对代码、图片类帖子格式化没那么好,可以考虑制作成PDF文档,然后发上来。
Gavin_Li
2011-04-25
这家伙很懒,什么也没写!
谢谢分享,剪切域确实很难搞懂。
joe3501
2014-08-16
这家伙很懒,什么也没写!
多谢楼主的分享,Mark
haitao52198
2014-08-16
这家伙很懒,什么也没写!
这部分确实挺难懂,顶起!
bigben
2014-09-06
这家伙很懒,什么也没写!
mark!
撰写答案
登录
注册新账号
关注者
0
被浏览
6k
关于作者
amsl
这家伙很懒,什么也没写!
提问
12
回答
137
被采纳
0
关注TA
发私信
相关问题
1
有关动态模块加载的一篇论文
2
最近的调程序总结
3
晕掉了,这么久都不见layer2的踪影啊
4
继续K9ii的历程
5
[GUI相关] FreeType 2
6
[GUI相关]嵌入式系统中文输入法的设计
7
20081101 RT-Thread开发者聚会总结
8
嵌入式系统基础
9
linux2.4.19在at91rm9200 上的寄存器设置
10
[转]基于嵌入式Linux的通用触摸屏校准程序
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
最新文章
1
开源共生 商业共赢 | RT-Thread 2024开发者大会议程正式发布!
2
【24嵌入式设计大赛】基于RT-Thread星火一号的智慧家居系统
3
RT-Thread EtherKit开源以太网硬件正式发布
4
如何在master上的BSP中添加配置yml文件
5
使用百度AI助手辅助编写一个rt-thread下的ONVIF设备发现功能的功能代码
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
WIZnet_W5500
UART
ota在线升级
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
msh
SFUD
rt_mq_消息队列_msg_queue
keil_MDK
ulog
MicroPython
C++_cpp
本月问答贡献
踩姑娘的小蘑菇
7
个答案
3
次被采纳
a1012112796
19
个答案
2
次被采纳
张世争
9
个答案
2
次被采纳
rv666
6
个答案
2
次被采纳
用户名由3_15位
13
个答案
1
次被采纳
本月文章贡献
程序员阿伟
9
篇文章
2
次点赞
hhart
3
篇文章
4
次点赞
RTT_逍遥
1
篇文章
6
次点赞
大龄码农
1
篇文章
5
次点赞
ThinkCode
1
篇文章
1
次点赞
回到
顶部
发布
问题
分享
好友
手机
浏览
扫码手机浏览
投诉
建议
回到
底部