精华内容
下载资源
问答
  • live2d-widget-models live2d模型的存储 使用情况 安装所有型号 使用npm install live2d-widget-models 请手动安装,此包装已被弃用,因为它不会增加生产环境的依赖性。 安装单独的模型 使用npm install {package...
  • Widget例子

    2017-12-27 22:58:59
    Widget例子。 本地IP地址的显示,以及自定义按钮。
  • django-json-widget 另一种可轻松编辑新Django字段JSONField(PostgreSQL特定模型字段)的小部件 快速开始 安装django-json-widget: pip install django-json-widget 将其添加到您的INSTALLED_APPS: INSTALLED_...
  • NULL 博文链接:https://hanllove001.iteye.com/blog/1185506
  • 基于QT的widget功能使用实例
  • qt widget控件拖放示例

    2019-02-25 22:13:43
    实现qt widget控件的拖放功能,并且有鼠标拖动过程控件截图
  • QT界面旋转切换Widget

    2018-06-06 14:36:49
    QT界面的旋转切换样例,可以实现旋转效果,在切换不同显示内容时效果较好。
  • 点击按钮实现不同widget间切换位置大小
  • android app widget demo

    2016-09-22 11:47:23
    1,widget 基本展示 2,widget点击跳转到activity 3,发送广播事件,更新widget界面
  • ThinkPHP的Widget扩展用于根据页面需要输出不同内容,它在项目目录中的Lib/Widget下定义。 具体定义如下: class NewsListWidget extends Widget{ public function render($data){ // code... } } 需要注意: 1...
  • Widget介绍.rar

    2019-07-09 23:00:15
    Widget由来、发展、用途、示例、开发 Widget是一种小插件,通常以小窗或小框的形式出现在网页、系统桌面、手机等地方。Widget通常使用的是HTML、Javascript、Flash或者iframe方式嵌入。一个界面可以有多个widget,...
  • Widget扩展用于在页面根据需要输出不同的内容,Widget扩展的定义是在项目的Lib\Widget目录下面定义Widget类库,例如下面定义了一个用于显示最近的评论的Widget: 位于Lib\Widget\ShowCommentWidget.class.php Widget...
  • org.vaadin.addons.dcharts-widget-0.10.0-dcharts-widget-0.10.0.jar
  • 本篇文章主要介绍了Android中的 widget 桌面组件开发教程,对AppWidget 框架以及AppWidgetManger类进行详细讲解,开发Android widget 开发的朋友可以参考下
  • dcharts-widget

    2016-06-21 11:41:34
    dcharts-widget
  • Widget 简介

    千次阅读 2021-11-05 16:57:05
    文章目录一、默认计数器应用 一、默认计数器应用 import 'package:flutter/material.dart'; //导包 void main() { runApp(const MyApp()); //应用入口 } /// 代表 Flutter 应用 ... Widget build(BuildCon

    一、默认计数器应用

    计数器执行流程:当右下角的floatingActionButton按钮被点击之后,会调用_incrementCounter方法。在_incrementCounter方法中,首先会自增_counter计数器(状态),然后setState会通知 Flutter 框架状态发生变化,接着,Flutter 框架会调用build方法以新的状态重新构建UI,最终显示在设备屏幕上。

    Flutter 中是通过 Widget 嵌套 Widget 的方式来构建UI和进行实践处理的,所以记住,Flutter 中万物皆为Widget。

    import 'package:flutter/material.dart'; //导包
    
    void main() {
      runApp(const MyApp()); //应用入口
    }
    
    /// 代表 Flutter 应用
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          //应用名称
          title: 'Flutter Demo',
          theme: ThemeData(
            //蓝色主题
            primarySwatch: Colors.blue,
          ),
          //应用首页路由
          home: const MyHomePage(title: 'Flutter Demo Home Page'),
        );
      }
    }
    
    /// 应用的首页
    class MyHomePage extends StatefulWidget {
      const MyHomePage({Key? key, required this.title}) : super(key: key);
      final String title;
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    /// MyHomePage类对应的状态类
    class _MyHomePageState extends State<MyHomePage> {
      int _counter = 0;
    
      //当按钮点击时,会调用此函数,该函数的作用是先自增_counter,然后调用setState 方法。
      // setState方法的作用是通知 Flutter 框架,有状态发生了改变,
      // Flutter 框架收到通知后,会执行 build 方法来根据新的状态重新构建界面
      void _incrementCounter() {
        setState(() {
          _counter++;
        });
      }
    
      // 构建UI界面的逻辑
      @override
      Widget build(BuildContext context) {
        // 1、Scaffold 是 Material 库中提供的页面脚手架,它提供了默认的导航栏、标题和包含主屏幕 widget 树(后同“组件树”或“部件树”)的body属性,组件树可以很复杂。
        // 2、body的组件树中包含了一个Center 组件,Center 可以将其子组件树对齐到屏幕中心。
        // 3、
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Center(
            // Center 子组件是一个Column 组件,Column的作用是将其所有子组件沿屏幕垂直方向依次排列;
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ],
            ),
          ),
          // floatingActionButton是页面右下角的带“+”的悬浮按钮,它的onPressed属性接受一个回调函数,代表它被点击后的处理器,
          // 本例中直接将_incrementCounter方法作为其处理函数。
          floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        );
      }
    }
    

    二、Widget 接口

    Widget 的部分源码。

    @immutable // 不可变的
    abstract class Widget extends DiagnosticableTree {
      const Widget({ this.key });
    
      final Key? key;
    
      @protected
      @factory
      Element createElement();
    
      @override
      String toStringShort() {
        final String type = objectRuntimeType(this, 'Widget');
        return key == null ? type : '$type-$key';
      }
    
      @override
      void debugFillProperties(DiagnosticPropertiesBuilder properties) {
        super.debugFillProperties(properties);
        properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
      }
    
      @override
      @nonVirtual
      bool operator ==(Object other) => super == other;
    
      @override
      @nonVirtual
      int get hashCode => super.hashCode;
    
      static bool canUpdate(Widget oldWidget, Widget newWidget) {
        return oldWidget.runtimeType == newWidget.runtimeType
            && oldWidget.key == newWidget.key;
      }
      ...
    }
    
    • @immutable 代表 Widget 是不可变的,这会限制 Widget 中定义的属性(即配置信息)必须是不可变的(final),为什么不允许 Widget 中定义的属性变化呢?这是因为,Flutter 中如果属性发生则会重新构建Widget树,即重新创建新的 Widget 实例来替换旧的 Widget 实例,所以允许 Widget 的属性变化是没有意义的,因为一旦 Widget 自己的属性变了自己就会被替换。这也是为什么 Widget 中定义的属性必须是 final 的原因。
    • Widget 类继承自 DiagnosticableTree,DiagnosticableTree 即“诊断树”,主要作用是提供调试信息。
    • Key: 这个 key 属性的主要的作用是决定是否在下一次 build时 复用旧的 Widget ,决定的条件在canUpdate() 方法中。
    • createElement():“一个 Widget 可以对应多个 Element”;Flutter 框架在构建 UI 树时,会先调用此方法生成对应节点的 Element 对象。此方法是 Flutter 框架隐式调用的,在我们开发过程中基本不会调用到。
    • debugFillProperties(…) 复写父类的方法,主要是设置诊断树的一些特性。
    • canUpdate(…) 是一个静态方法,它主要用于在 Widget 树重新 build 时复用旧的 widget ,其实具体来说,应该是:是否用新的 Widget 对象去更新旧 UI 树上所对应的 Element 对象的配置;通过其源码我们可以看到,只要 newWidget 与 oldWidget 的 runtimeType 和 key 同时相等时就会用 newWidget 去更新Element 对象的配置,否则就会创建新的 Element。

    三、三棵树

    Flutter 中有三棵树:Widget 树,Element 树和 RenderObject 树。当应用启动时 Flutter 会遍历并创建所有的 Widget 形成 Widget Tree,同时与 Widget Tree 相对应,通过调用 Widget 上的 createElement() 方法创建每个 Element 对象,形成 Element Tree。最后调用 Element 的 createRenderObject() 方法创建每个渲染对象,形成一个 Render Tree。 Element就是Widget在UI树具体位置的一个实例化对象,大多数Element只有唯一的renderObject,但还有一些Element会有多个子节点,如继承自RenderObjectElement的一些类,比如 MultiChildRenderObjectElement。最终所有 Elemen t的RenderObject 构成一棵树,我们称之为”Render Tree“即”渲染树“。总结一下,我们可以认为 Flutter 的UI 系统包含三棵树:Widget 树、Element 树、渲染树。他们的依赖关系是:根据 Widget 树生成Element 树,再依赖于 Element 树生成 RenderObject 树。
    在这里插入图片描述
    在 flutter 中,Container、Text 等组件都属于 Widget,所以这课树就是 Widget 树,也可以叫做控件树,它就表示了我们在 dart 代码中所写的控件的结构。Element 就是 Widget 的另一种抽象。我们在代码中使用的像 Container、Text 等这类组件和其属性只不过是我们想要构建的组件的配置信息,当我们第一次调用 build()`方法想要在屏幕上显示这些组件时,Flutter 会根据这些信息生成该 Widget 控件对应的 Element,同样地,Element 也会被放到相应的 Element 树当中。RenderObject 在 Flutter 当中做组件布局渲染的工作,其为了组件间的渲染搭配及布局约束也有对应的 RenderObject 树,我们也称之为渲染树。

    作者:邱穆
    链接:https://www.jianshu.com/p/e2c2ea310bdc

    四、StatelessWidget 和 StatefulWidget

    在 Flutter 中,Widget 分为两类:Stateless(无状态)和 Stateful(有状态)Widget。StatelessWidget 没有内部状态,Icon、IconButton, 和 Text 都是无状态 Widget, 它们都是 StatelessWidget 的子类。StatefulWidget 是动态的,用户可以和其交互(例如输入一个表单、 或者移动一个 slider 滑块),或者可以随时间改变 (也许是数据改变导致的 UI 更新)。Checkbox, Radio, Slider, InkWell, Form, and TextField 都是 Stateless widgets, 它们都是 StatefulWidget 的子类。

    StatelessWidget
    StatelessWidget 是不可变的, 这意味着它们的属性不能改变——所有的值都是最终的。如果无状态Widget 里面有子 Widget,并且子 Widget 是有状态的,则子 Widget 的内容是可以通过 setState 来更改的。无状态 Widget 影响的仅仅是自己是无状态的,不会影响他的父 Widget 和子 Widget。

    StatefulWidget
    StatefulWidget 持有的状态可能在 Widget 生命周期中发生变化。

    State
    一个 StatefulWidget 类会对应一个 State 类,State 表示与其对应的 StatefulWidget 要维护的状态,State 中的保存的状态信息可以:

    • 在 Widget 构建时可以被同步读取。
    • 在 Widget 生命周期中可以被改变,当 State 被改变时,可以手动调用其 setState() 方法通知 Flutter 框架状态发生改变,Flutter 框架在收到消息后,会重新调用其 build 方法重新构建 Widget 树,从而达到更新 UI 的目的。

    State 中有两个常用属性:

    • widget ,它表示与该 State 实例关联的 Widget 实例,由 Flutter 框架动态设置。注意,这种关联并非永久的,因为在应用生命周期中,UI 树上的某一个节点的 Widget 实例在重新构建时可能会变化,但State 实例只会在第一次插入到树中时被创建,当在重新构建时,如果 Widget 被修改了,Flutter 框架会动态设置State,widget 为新的 Widget 实例。
    • context。StatefulWidget 对应的 BuildContext,作用同 StatelessWidget 的 BuildContext。

    五、Widget 生命周期

    Widget 生命周期主要分为 StatefullWidget 的生命周期和 StatelessWidget 的生命周期,StatelessWidget 的生命周期比较简单,它只有一个 build 方法,我们在这儿不做过多关注,以下主要分析 StatefullWidget 的生命周期。

    仍然以计数器功能为例,实现一个计数器 CounterWidget 组件 ,点击它可以使计数器加1,由于要保存计数器的数值状态,所以我们应继承 StatefulWidget。

    import 'package:flutter/material.dart'; //导包
    
    void main() {
      runApp(const MyApp()); //应用入口
    }
    
    /// 代表 Flutter 应用
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          home: StateLifecycleTest(),
        );
      }
    }
    
    /// 路由
    class StateLifecycleTest extends StatelessWidget {
      const StateLifecycleTest({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return const CounterWidget();
      }
    }
    
    /// 计数器组件
    class CounterWidget extends StatefulWidget {
      const CounterWidget({Key? key, this.initValue = 0});
    
      final int initValue;
    
      @override
      _CounterWidgetState createState() => _CounterWidgetState();
    }
    
    /// State 代码
    class _CounterWidgetState extends State<CounterWidget> {
      int _counter = 0;
    
      @override
      void initState() {
        super.initState();
        //初始化状态
        _counter = widget.initValue;
        print("initState");
      }
    
      @override
      Widget build(BuildContext context) {
        print("build");
        return Scaffold(
          body: Center(
            child: TextButton(
              child: Text('$_counter'),
              //点击后计数器自增
              onPressed: () => setState(
                () => ++_counter,
              ),
            ),
          ),
        );
      }
    
      @override
      void didUpdateWidget(CounterWidget oldWidget) {
        super.didUpdateWidget(oldWidget);
        print("didUpdateWidget ");
      }
    
      @override
      void deactivate() {
        super.deactivate();
        print("deactivate");
      }
    
      @override
      void dispose() {
        super.dispose();
        print("dispose");
      }
    
      @override
      void reassemble() {
        super.reassemble();
        print("reassemble");
      }
    
      @override
      void didChangeDependencies() {
        super.didChangeDependencies();
        print("didChangeDependencies");
      }
    }
    

    监听 App 生命周期

    1、组合 WidgetsBindingObserver 类。

    class _CounterWidgetState extends State<CounterWidget> with WidgetsBindingObserver
    

    2、在 initState 中添加监听。

    WidgetsBinding.instance.addObserver(this);
    

    3、在 dispose 中添加监听。

    WidgetsBinding.instance.removeObserver(this);
    

    4、State 中实现状态监听。

    @override
    void didChangeAppLifecycleState(AppLifecycleState state) {
      super.didChangeAppLifecycleState(state);
      if (state == AppLifecycleState.paused) {
        // The application is not currently visible to the user, not responding to
        // user input, and running in the background.
        // 不可见,不可操作
      }
      if (state == AppLifecycleState.resumed) {
        // The application is visible and responding to user input.
        // 可见,可操作
      }
      if (state == AppLifecycleState.inactive) {
        // The application is in an inactive state and is not receiving user input.
        // 可见,不可操作
      }
      if (state == AppLifecycleState.detached) {
        // The application is still hosted on a flutter engine but is detached from any host views.
        // 虽然还在运行,但已经没有任何存在的界面。
      }
    }
    

    使用场景:
    场景 1:前台转后台:

    • AppLifecycleState.inactive
    • AppLifecycleState.paused

    场景 2:后台转前台:

    • AppLifecycleState.inactive
    • AppLifecycleState.resumed

    生命周期方法说明

    1. constructor:构造函数
    2. createState:createState 是 StatefulWidget 里创建 State 的方法,当要创建新的 StatefulWidget 的时候,会立即执行 createState,而且只执行一次。
    3. initState:这个方法是组件创建后的第一个方法,它相当于原生的 onCreate 和 viewDidLoad,大部分页面初始逻辑调用会写到这个方法中。
    4. didChangeDependencies:当 State 对象的依赖发生变化时会被调用。例:你的 StatefulWidget 依赖一个 InheritedWidget 的数据,当数据发生变化时,会调用子 Widget 的该方法。第一次打开页面时 initState 之后会立刻调用该方法。
    5. didUpdateWidget:当父组件改变并且需要重新绘制 UI 时,这个方法会调用。通过这个方法,你可以通过参数获取到旧的组件,你可以通过对比新旧组件后做一些额外的逻辑。
    6. build:这个方法是最为重要的一个方法,它用来构建你的整个组件树。
    7. deactivate:当 State 对象从树中被移除时,会调用此回调。有些情况下,framework 会将该 State 插入到树的其它部分。
    8. dispose:当 State 对象从树中被永久移除时调用。通常在此回调中释放资源。
    9. reassemble:此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在 Release 模式下永远不会被调用。

    使用场景
    场景 1:打开页面:

    1. constructor
    2. createState
    3. initState
    4. didChangeDependencies
    5. build

    场景 2:退出页面:

    1. deactivate
    2. dispose

    场景 3:热重载:

    1. reassemble
    2. didUpdateWidget
    3. build

    场景 4:横竖屏切换:

    1. didUpdateWidget
    2. build
    3. didUpdateWidget
    4. build

    ————————————————
    原文链接:https://blog.csdn.net/haha223545/article/details/105483176

    六、在 widget 树中获取 State 对象

    有两种方法在子 widget 树中获取父级 StatefulWidget 的State 对象。

    通过 Context 获取
    一般来说,如果 StatefulWidget 的状态是私有的(不应该向外部暴露),那么我们代码中就不应该去直接获取其 State 对象;如果 StatefulWidget 的状态是希望暴露出的(通常还有一些组件的操作方法),我们则可以去直接获取其 State 对象。但是通过 context.findAncestorStateOfType 获取 StatefulWidget 的状态的方法是通用的,我们并不能在语法层面指定 StatefulWidget 的状态是否私有,所以在 Flutter 开发中便有了一个默认的约定:如果 StatefulWidget 的状态是希望暴露出的,应当在 StatefulWidget 中提供一个 of 静态方法来获取其 State 对象,开发者便可直接通过该方法来获取;如果 State不希望暴露,则不提供 of 方法。这个约定在 Flutter SDK 里随处可见。

    import 'package:flutter/material.dart'; //导包
    
    void main() {
      runApp(const MyApp()); //应用入口
    }
    
    /// 代表 Flutter 应用
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          //应用首页路由
          home: GetStateObjectRoute(),
        );
      }
    }
    
    class GetStateObjectRoute extends StatefulWidget {
      const GetStateObjectRoute({Key? key}) : super(key: key);
    
      @override
      State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
    }
    
    class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("子树中获取State对象"),
          ),
          body: Center(
            child: Column(
              children: [
                Builder(builder: (context) {
                  return ElevatedButton(
                    onPressed: () {
                      // 查找父级最近的Scaffold对应的ScaffoldState对象
                      ScaffoldState _state =
                          context.findAncestorStateOfType<ScaffoldState>()!;
                      // 打开抽屉菜单
                      _state.openDrawer();
                    },
                    child: Text('打开抽屉菜单1'),
                  );
                }),
                Builder(builder: (context) {
                  return ElevatedButton(
                    onPressed: () {
                      // 直接通过of静态方法来获取ScaffoldState
                      ScaffoldState _state = Scaffold.of(context);
                      // 打开抽屉菜单
                      _state.openDrawer();
                    },
                    child: Text('打开抽屉菜单2'),
                  );
                }),
                Builder(builder: (context) {
                  return ElevatedButton(
                    onPressed: () {
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(content: Text("我是SnackBar")),
                      );
                    },
                    child: Text('显示SnackBar'),
                  );
                }),
              ],
            ),
          ),
          drawer: Drawer(),
        );
      }
    }
    

    在这里插入图片描述
    通过 GlobalKey 获取
    GlobalKey 是 Flutter 提供的一种在整个 App 中引用 element 的机制。如果一个 widget 设置了GlobalKey,那么我们便可以通过 globalKey.currentWidget 获得该 widget 对象、globalKey.currentElement 来获得 widget 对应的 element 对象,如果当前 widget 是 StatefulWidget,则可以通过 globalKey.currentState 来获得该 widget 对应的state对象。

    注意:使用 GlobalKey 开销较大,如果有其他可选方案,应尽量避免使用它。另外,同一个 GlobalKey 在整个 widget 树中必须是唯一的,不能重复。

    import 'package:flutter/material.dart'; //导包
    
    void main() {
      runApp(const MyApp()); //应用入口
    }
    
    /// 代表 Flutter 应用
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          //应用首页路由
          home: GetStateObjectRoute(),
        );
      }
    }
    
    class GetStateObjectRoute extends StatefulWidget {
      const GetStateObjectRoute({Key? key}) : super(key: key);
    
      @override
      State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
    }
    
    class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
      //定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
      static GlobalKey<ScaffoldState> _globalKey = GlobalKey();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          key: _globalKey, //设置key
          appBar: AppBar(
            title: Text("子树中获取State对象"),
          ),
          body: Center(
            child: Column(
              children: [
                Builder(builder: (context) {
                  return ElevatedButton(
                    onPressed: () {
                      //通过GlobalKey来获取State对象
                      _globalKey.currentState?.openDrawer();
                    },
                    child: Text('打开抽屉菜单1'),
                  );
                }),
              ],
            ),
          ),
          drawer: Drawer(),
        );
      }
    }
    

    在这里插入图片描述

    七、通过 RenderObject 自定义 Widget

    StatelessWidget 和 StatefulWidget 都是用于组合其它组件的,它们本身没有对应的 RenderObject。Flutter 组件库中的很多基础组件都不是通过 StatelessWidget 和 StatefulWidget 来实现的,比如 Text 、Column、Align 等。

    如果组件不会包含子组件,则我们可以直接继承自 LeafRenderObjectWidget ,它是 RenderObjectWidget 的子类,而 RenderObjectWidget 继承自 Widget 。

    class CustomWidget extends LeafRenderObjectWidget{
      @override
      RenderObject createRenderObject(BuildContext context) {
        // 创建 RenderObject
        return RenderCustomObject();
      }
      @override
      void updateRenderObject(BuildContext context, RenderCustomObject  renderObject) {
        // 更新 RenderObject
        super.updateRenderObject(context, renderObject);
      }
    }
    
    class RenderCustomObject extends RenderBox{
    
      @override
      void performLayout() {
        // 实现布局逻辑
      }
    
      @override
      void paint(PaintingContext context, Offset offset) {
        // 实现绘制
      }
    }
    

    如果自定义的 widget 可以包含子组件,则可以根据子组件的数量来选择继承SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget。通常,我们的 Widget 可以继承自以下三种类

    • SingleChildRenderObjectWidget:RenderObject只有一个 child。
    • MultiChildRenderObjectWidget:可以有多个 child。
    • LeafRenderObjectWidget :RenderObject 是一个叶子节点,没有 child。
      ————————————————
      原文链接:https://blog.csdn.net/yingshukun/article/details/107814111

    八、Flutter SDK 内置组件库介绍

    Flutter 提供了一套丰富、强大的基础组件,在基础组件库之上 Flutter 又提供了一套 Material 风格( Android 默认的视觉风格)和一套 Cupertino 风格(iOS视觉风格)的组件库。要使用基础组件库,需要先导入:

    import 'package:flutter/widgets.dart';
    

    基础组件

    • Text (opens new window):创建一个带格式的文本。
    • Row (opens new window)、 Column (opens new window): 这些具有弹性空间的布局类 widget 可让您在水平(Row)和垂直(Column)方向上创建灵活的布局。
    • Stack (opens new window): 取代线性布局 (和 Android 中的 FrameLayout 相似),Stack 允许子 widget 堆叠, 你可以使用 Positioned (opens new window)来定位他们相对于 Stack 的上下左右四条边的位置。
    • Container (opens new window): Container (opens new window)可让您创建矩形视觉元素。Container 可以装饰一个 BoxDecoration (opens new window), 如 background、一个边框、或者一个阴影。 Container (opens new window)也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。另外, Container (opens new window)可以使用矩阵在三维空间中对其进行变换。

    Material 组件
    Flutter 提供了一套丰富 的 Material 组件,它可以帮助我们构建遵循 Material Design 设计规范的应用程序。Material 应用程序以 MaterialApp (opens new window) 组件开始, 该组件在应用程序的根部创建了一些必要的组件,比如 Theme 组件,它用于配置应用的主题。 是否使用 MaterialApp (opens new window)完全是可选的,但是使用它是一个很好的做法。要使用 Material 组件,需要先引入它:

    import 'package:flutter/material.dart';
    

    Cupertino 组件
    Flutter 也提供了一套丰富的 Cupertino 风格的组件,尽管目前还没有 Material 组件那么丰富,但是它仍在不断的完善中。值得一提的是在 Material 组件库中有一些组件可以根据实际运行平台来切换表现风格,比如 MaterialPageRoute,在路由切换时,如果是 Android 系统,它将会使用 Android 系统默认的页面切换动画(从底向上);如果是 iOS 系统,它会使用 iOS 系统默认的页面切换动画(从右向左)。

    展开全文
  • Flutter Widget详解

    万次阅读 2020-09-24 19:39:05
    Flutter Widget详解 Widget是Flutter应用程序用户界面的基本构建块。每个Widget都是用户界面一部分的不可变声明。 与其他将视图、控制器、布局和其他属性分离的框架不同,Flutter具有一致的统一对象模型:widget。 ...

    Flutter Widget详解


    Widget是Flutter应用程序用户界面的基本构建块。每个Widget都是用户界面一部分的不可变声明。 与其他将视图、控制器、布局和其他属性分离的框架不同,Flutter具有一致的统一对象模型:widget。

    Flutter Widget采用现代响应式框架构建,这是从 React 中获得的灵感,中心思想是用widget构建你的UI。 Widget描述了他们的视图在给定其当前配置和状态时应该看起来像什么。当widget的状态发生变化时,widget会重新构建UI,Flutter会对比前后变化的不同, 以确定底层渲染树从一个状态转换到下一个状态所需的最小更改。

    在编写应用程序时,通常会创建新的widget,这些widget是无状态的StatelessWidget或者是有状态的StatefulWidget, 具体的选择取决于您的widget是否需要管理一些状态。widget的主要工作是实现一个build函数,用以构建自身。一个widget通常由一些较低级别widget组成。Flutter框架将依次构建这些widget,直到构建到最底层的子widget时,这些最低层的widget通常为RenderObject,它会计算并描述widget的几何形状。

    基础 Widget

    Flutter有一套丰富、强大的基础widget,其中以下是很常用的:

    • Text:该 widget 可让创建一个带格式的文本。
    • Row、 Column: 这些具有弹性空间的布局类Widget可让您在水平(Row)和垂直(Column)方向上创建灵活的布局。其设计是基于web开发中的Flexbox布局模型。
    • Stack: 取代线性布局 (和Android中的LinearLayout相似),Stack允许子 widget 堆叠, 你可以使用 Positioned 来定位他们相对于Stack的上下左右四条边的位置。Stacks是基于Web开发中的绝度定位(absolute positioning )布局模型设计的。
    • Container: Container 可让您创建矩形视觉元素。container 可以装饰为一个BoxDecoration, 如 background、一个边框、或者一个阴影。 Container 也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。另外, Container可以使用矩阵在三维空间中对其进行变换。

    使用 Material 组件

    Flutter提供了许多widgets,可帮助您构建遵循Material Design的应用程序。Material应用程序以MaterialApp widget开始, 该widget在应用程序的根部创建了一些有用的widget,其中包括一个Navigator, 它管理由字符串标识的Widget栈(即页面路由栈)。Navigator可以让您的应用程序在页面之间的平滑的过渡。 是否使用MaterialApp完全是可选的,但是使用它是一个很好的做法。

    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          home: MyHomePage(title: 'budaye的列表'),
        );
      }
    }
    

    构建widget

    你可以通过实现widget的build返回widget树(或层次结构)来定义widget的独特特征。这棵树更具体地表示了用户界面的widget层次。例如,工具栏widget的build函数可能返回一个包含一些文本和各种按钮的水平布局。然后,框架递归地构建widget,直到该所有widget构建完成,然后framework将他们一起添加到树中。

    widget的构建函数一般没有副作用。每当它被要求构建时,widget应该返回一个新的widget树,无论widget以前返回的是什么。Framework会将之前的构建与当前构建进行比较并确定需要对用户界面进行哪些修改。

    这种自动比较非常有效,可以实现高性能的交互式应用程序。而构建函数的设计则着重于声明widget是由什么构成的,而不是将用户界面从一个状态更新到另一个状态的(这很复杂性),从而简化了代码。

    处理手势

    手势处理是APP开发中的常用的操作,例如,当用户需要与应用进行交互时(单击、长按等),就需要我们对手势检测并作出处理。

    手势检测,我们可以使用GestureDetector widget,它并不具有显示效果,而是可以检测由用户做出的手势。

    我们来看一个示例:

    class _MyHomePageState extends State<MyHomePage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
              backgroundColor: Colors.green, //设置标题栏的背景颜色
              title: new Title(
                child: new Text(
                  '这是一个标题',
                  style: new TextStyle(
                    fontSize: 20.0,
                    color: Colors.white,
                  ),
                ),
                color: Colors.white,
              ),
            ),
            body: new GestureDetector( //设置事件
              child: new Center(
                child: new Icon( //图标
                  Icons.account_circle, //设置图标内容
                  color: Colors.red, //设置图标的颜色
                ),
              ),
              onTap: () {
                print("点击");
              },
            ),
        );
      }
    }
    

    我们在屏幕中间放置一个图标作为用户点击的对象,它放置在了Center中,以居中显示。外层使用GestureDetector进行包裹,当用户点击图标时,就会触发回调函数onTap,我们可以在onTap中添加我们的处理逻辑,例如,这里打印出了一行文本。

    另外,我们还可以使用GestureDetector来检测各种输入手势,包括点击、拖动和缩放。

    许多widget都会使用一个GestureDetector为其他widget提供可选的回调。 例如,IconButton、 RaisedButton、 和FloatingActionButton ,它们都有一个onPressed回调,它会在用户点击该widget时被触发。

    widget状态的改变

    widget分为无状态widget和有状态widget两类。

    无状态widget从它们的父widget接收参数,它们被存储在final型的成员变量中,当一个widget被要求构建时,它使用这些存储的值作为参数来构建widget。

    有状态的widget可以构建更复杂的体验——例如,以更有趣的方式对用户输入做出反应,应用程序通常会携带一些状态。 Flutter使用StatefulWidgets来满足这种需求。StatefulWidgets是特殊的widget,它知道如何生成State对象,然后用它来保持状态。

    class MyHomePage extends StatefulWidget {
      MyHomePage({Key key, this.title}) : super(key: key);
      final String title;
    
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      Color currentColor = Colors.red;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Colors.green, //设置标题栏的背景颜色
            title: new Title(
              child: new Text(
                '这是一个标题',
              ),
              color: Colors.white,
            ),
          ),
          body: new GestureDetector(
            //设置事件
            child: new Center(
              child: new Icon(
                //图标
                Icons.account_circle, //设置图标内容
                color: currentColor, //设置图标的颜色
              ),
            ),
            onTap: () {
              print("点击");
              setState(() {
                if (currentColor == Colors.red) {
                  currentColor = Colors.green;
                } else {
                  currentColor = Colors.red;
                }
              });
            },
          ),
        );
      }
    }
    

    在Flutter中,StatefulWidget和State是单独的对象,这两种类型的对象具有不同的生命周期:Widget是临时对象,用于构建当前状态下的应用程序,而State对象在多次调用build()之间保持不变,允许它们记住信息(状态)。

    在Flutter中,事件流是“向上”传递的,而状态流是“向下”传递的,重定向这一流程的共同父元素是State。

    注意:我们通常命名State子类时带一个下划线,这表示其是私有的。

    State的创建于销毁

    在StatefulWidget调用createState之后,框架将新的状态对象插入树中,然后调用状态对象的initState。 子类化State可以重写initState,以完成仅需要执行一次的工作。 例如,您可以重写initState以配置动画或订阅platform services。initState的实现中需要调用super.initState。

    当一个状态对象不再需要时,框架调用状态对象的dispose。 您可以覆盖该dispose方法来执行清理工作。例如,您可以覆盖dispose取消定时器或取消订阅platform services。 dispose典型的实现是直接调用super.dispose。

    Key

    您可以使用key来控制框架将在widget重建时与哪些其他widget匹配。默认情况下,框架根据它们的runtimeType和它们的显示顺序来匹配。 使用key时,框架要求两个widget具有相同的key和runtimeType。

    Key在构建相同类型widget的多个实例时很有用。例如,ShoppingList构建足够的ShoppingListItem实例以填充其可见区域:

    • 如果没有key,当前构建中的第一个条目将始终与前一个构建中的第一个条目同步,即使在语义上,列表中的第一个条目如果滚动出屏幕,那么它将不会再在窗口中可见。

    • 通过给列表中的每个条目分配为“语义” key,无限列表可以更高效,因为框架将同步条目与匹配的语义key并因此具有相似(或相同)的可视外观。 此外,语义上同步条目意味着在有状态子widget中,保留的状态将附加到相同的语义条目上,而不是附加到相同数字位置上的条目。

    您可以使用全局key来唯一标识子widget。全局key在整个widget层次结构中必须是全局唯一的,这与局部key不同,后者只需要在同级中唯一。由于它们是全局唯一的,因此可以使用全局key来检索与widget关联的状态。

    展开全文
  • widget小组件demo

    2015-11-16 16:12:47
    android widget小组件,widget 和app界面通过广播,服务(service),RemoteViews 交互
  • 头脑的智慧!前人之思想
  • 添加widget到界面,完成的源代码,实现可以动态添加widget
  • QT的Listwidget控件使用

    千次阅读 2021-03-23 15:24:09
    另一类是 Item Widgets,包括 QListWidget、QTreeWidget 和 QTable Widget。QListWidget其实是QListView的遍历类,QListView 是基于模型/视图(Model/View)结构,视图(View)与模型数据(Model Data)关联实现数据的...

    一、简介

    Qt 中用于项(Item)处理的组件有两类,一类是 Item Views,包括 QListView、QTreeView、 QTableView、QColumnView 等;另一类是 Item Widgets,包括 QListWidget、QTreeWidget 和 QTable Widget。QListWidget其实是QListView的遍历类,QListView 是基于模型/视图(Model/View)结构,视图(View)与模型数据(Model Data)关联实现数据的显示和编辑;而QListWidget 可直接对每一项直接操作,所以对于一些简单的界面显示,可采用QListWidget ,如果对于一些复杂的显示,实现数据和界面显示分离,可采用QListView 。
    下面给大家介绍QListWidget控件的常见使用。

    二、属性

    在UI设计器上,手动拉一个ListWidget控件,如图:
    在这里插入图片描述
    双击 ListWidget 组件,可以打开其列表项编辑器,如图 所示。在这个编辑器里可以增加、删除、上移、下移列表项,可以设置每个项的属性,包括文字内容、字体、文字对齐方式、背景色、前景色等。
    在这里插入图片描述
    比较重要的是其 flags 属性(如图 3 所示),用于设置项的一些标记,这些标记是枚举类型 Qt::ItemFlag 的具体值,包括以下几种:

    • Selectable:项是否可被选择,对应枚举值 Qt::ItemIsSelectable。
    • Editable:项是否可被编辑,对应枚举值 Qt:: ItemlsEditable。
    • DragEnabled:项是否可以被拖动,对应枚举值 Qt:: ItemlsDragEnabled。
    • DropEnabled:项是否可以接收拖放的项,对应枚举值 Qt:: ItemlsDropEnabled。

    UserCheckable:项是否可以被复选,若为 true,项前面出现一个 CheckBox,对应枚举值 Qt::ItemlsUserCheckable。
    Enabled:项是否被使能,对应枚举值 Qt:: ItemlsEnabled。
    Tristate:是否允许 Check 的第三种状态,若为 false,则只有 checked 和 unchecked 两种状态,对应枚举值 Qt::ItemIsAutoTristate。
    在代码中设置项的 flags 属性时,使用函数 setFlags(),例如:

    aItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable |Qt::ItemIsEnabled);
    

    不过QListWidget 的列表项一般是在程序里动态创建。所以下面通过一个例子给大家介绍该控件的动态创建。

    三、实例

    UI界面如图:
    在这里插入图片描述
    1、首先是实现插入项的功能,也就是新增一项,代码如下:

    void Widget::on_InsertBtn_clicked()
    {
        //首先是获取项的总数
        int itemCount = ui->listWidget->count();
    
        //new一个空的子项
        QListWidgetItem * item = new QListWidgetItem;
        //初始化空的子项,比如设置颜色,高度,内容等等
        //设置大小
        item->setSizeHint(QSize(ui->listWidget->width(),50));
        //设置内容
        item->setText(QString("自定义子项%1").arg(itemCount));
        //设置属性
        item->setFlags(Qt::ItemIsEditable|Qt::ItemIsEnabled|Qt::ItemIsUserCheckable);//双击可被编辑,可选中
        item->setCheckState(Qt::Unchecked);//默认 不选中状态
    
        //最后将初始化好的子项插入到listWidget控件中
        ui->listWidget->addItem(item);
    }
    

    2、删除项,删除当前选中的项,代码如下:

    void Widget::on_DelBtn_clicked()
    {
        //先判断当前是否选中,如果没有选中则提示
        if(!ui->listWidget->currentItem()){
            QMessageBox::warning(this,"警告","请先选中当前项再进行删除!");
            return;
        }
    
        //获取当前想要删除的子项
        QListWidgetItem * delItem = ui->listWidget->takeItem(ui->listWidget->currentRow());
        if(delItem)//注意需要手动删除
            delete delItem;
    }
    

    这里需要注意的是takeItem函数,查看QT官方手册介绍:

    QListWidgetItem *QListWidget::takeItem(int row)
    Removes and returns the item from the given row in the list widget; otherwise returns nullptr.
    Items removed from a list widget will not be managed by Qt, and will need to be deleted manually.
    

    大体的意思是:
    从列表小部件的给定行中移除并返回项;否则返回nullptr。
    从列表小部件中删除的项目将不由Qt管理,需要手动删除。

    所以在这里我们需要手动删除子项。

    3、清空列表,调用clear()函数即可。

    void Widget::on_ClearBtn_clicked()
    {
        ui->listWidget->clear();
    }
    
    

    4、状态栏的显示

    void Widget::on_listWidget_currentRowChanged(int currentRow)
    {
        //如果没有子项,直接退出
        if(currentRow<0)
            return;
    
        //获取当前项
        QListWidgetItem *curItem = ui->listWidget->currentItem();
        //获取内容
        QString curText = curItem->text();
        //在状态栏进行显示内容和选中状态
        QString stateText;
        stateText = (QString("当前选中第%1项:").arg(currentRow)+curText);
        if(curItem->checkState() == Qt::Checked)
            stateText += "  选中";
        else
            stateText += "  没有选中";
    
        ui->StateLabel->setText(stateText);
    }
    

    5、右键菜单功能显示
    首先在构造函数生效,自定义菜单策略,代码如下:

        //设置控件的自定义菜单策略
        ui->listWidget->setContextMenuPolicy(Qt::CustomContextMenu);
    

    当用户点击鼠标右键时,listWidget控件 会发射信号customContextMenuRequested(const QPoint &pos),关联槽函数,代码如下:

    void Widget::on_listWidget_customContextMenuRequested(const QPoint &pos)
    {
        //定义菜单类,listWidget控件为父类,作用就是把菜单和控件进行绑定
        QMenu *m_menu = new QMenu(ui->listWidget);
        QAction *act;
        //new一个Action功能类,菜单的子项
        act = new QAction(this);
        act->setText("插入项");
        //当触发该子项时,执行对应的槽函数
        connect(act,SIGNAL(triggered()),this,SLOT(on_InsertBtn_clicked()));
        //将该子项插入菜单中
        m_menu->addAction(act);
    
        //执行菜单,菜单的位置在当前光标位置上
        m_menu->exec(QCursor::pos());
        delete m_menu;//执行完毕删除菜单
    }
    

    6、常用设置

    排序

        ui->listWidget->setSortingEnabled(true);//生效排序
        ui->listWidget->sortItems(Qt::DescendingOrder); //降序
    

    显示方式

        ui->listWidget->setViewMode(QListView::IconMode);
    

    该实例可在博主的博客资源中下载(关注可下载)。资源名称:Listwidget控件实例1

    四、缩略图项目

    1、UI设计
    在这里插入图片描述
    2、初始化

    void Widget::InitListwidget()
    {
        //自定义菜单策略
        ui->listWidget->setContextMenuPolicy(Qt::CustomContextMenu);
        //设置视图显示方式为图标模式(图标在上,文字在下)
        ui->listWidget->setViewMode(QListView::IconMode);
        //设置可以选择多项
        ui->listWidget->setSelectionMode(QAbstractItemView::ExtendedSelection);
        //设置背景颜色,使每个子项之间的间距有颜色区别
        //ui->listWidget->setStyleSheet("background:transparent;");
        //设置无边框
        ui->listWidget->setFrameShape(QFrame::NoFrame);
        //设置子项不能被拖拽
        ui->listWidget->setDragEnabled(false);
        //设置子项每个图标的大小
        ui->listWidget->setIconSize(QSize(200,200));
        //设置每个子项的大小固定
        ui->listWidget->setUniformItemSizes(true);
        //设置QLisView大小改变时,图标的调整模式,默认是固定的,可以改成自动调整
        ui->listWidget->setResizeMode(QListView::Adjust);
        //设置每个子项之间的间距
        ui->listWidget->setSpacing(10);
    
        //设置样式,直接在函数中设置
        ui->listWidget->setStyleSheet("QListWidget{border:1px solid gray; color:black;background:transparent;}"
                                   "QListWidget::Item{padding-top:20px; padding-bottom:4px; }"
                                   "QListWidget::Item:hover{background:skyblue; }"
                                   "QListWidget::item:selected{background:lightgray; color:red; }"
                                   "QListWidget::item:selected:!active{border-width:0px; background:lightgreen; }"
                                   );
    
    }
    

    2、打开文件

    //打开文件
    void Widget::on_OpenBtn_clicked()
    {
        QStringList fileNameList = QFileDialog::getOpenFileNames(this,"文件",".","图片文件(*.png *.jpg *.bmp)");
        if(fileNameList.isEmpty())
            return;
    
        for(int i=0; i<fileNameList.count(); i++)
        {
            //创建子项,将图片作为icon,文件名作为文字描述
            QListWidgetItem *item = new QListWidgetItem;
            //设置子项
            //图片作为子项的图标
            QPixmap pic(fileNameList.at(i));
    
            item->setIcon(QIcon(pic.scaled(QSize(200,200))));
            //item->setSizeHint(QSize(100,50));
            //文件名作为子项的文字描述
            QFileInfo file(fileNameList.at(i));
            item->setText(file.fileName());
            //设置文字对齐方向,水平居中
            item->setTextAlignment(Qt::AlignHCenter);
    
            //将item子项插入 listwidget控件中,或者在new子项的时候传入父控件相当于添加item
            ui->listWidget->addItem(item);
        }
    }
    

    3、删除文件

    //删除文件
    void Widget::on_DelBtn_clicked()
    {
        //先判断当前列表是否为空
        if(ui->listWidget->count()==0)
            return;
        //获取当前要删除的子项,可多选
        QList<QListWidgetItem*> delItemList = ui->listWidget->selectedItems();
    
        for(int i=0; i<delItemList.count(); i++)
        {
            //takeItem只是将该项从listwidget控件中移除,所以还要手动删除
            QListWidgetItem *delItem = ui->listWidget->takeItem(ui->listWidget->row(delItemList.at(i)));
            if(delItem)
                delete delItem;
        }
    }
    

    4、清空文件

    void Widget::on_ClearBtn_clicked()
    {
        ui->listWidget->clear();
    }
    

    该实例可在博主的博客资源中下载(关注可下载)。资源名称:
    <使用listwidget控件制作的缩略图项目>

    五、高级使用

    在我们平时开发过程中,一般会自定义listwidget控件的子项和子项的窗口,详情请下载博主的源码

    展开全文
  • 谷歌新推出的复合MaterialDesign设计规范的包叫android.support.design.widget 。这是其中TabLayout的典型用法
  • android AppWidget ListView

    热门讨论 2013-11-06 21:39:30
    android app widget 使用listview 源码
  • APICloud开发Widget包结构说明
  • Widget之间通信平台的研究与应用,夏姣姣,,目前互联网的Widget主要包括以iGoogle为代表的Web Widget以及以Yahoo Widget为代表的桌面Widget,本文主要研究Web Widget。现有的主流Web Widget主要��
  • 利用vtkwidget 实现剪裁

    千次阅读 热门讨论 2020-08-17 19:43:26
      有网友私信说想利用vtkBoxWidget和vtkExtractVOI实现剪裁。之前介绍过vtkBoxWidget,主要是说基本功能和...这里写目录标题1 利用vtkwidget 剪裁步骤2 利用vtkwidget 剪裁模型展示3 利用vtkwidget 剪裁模型代码4 利

      想利用vtkBoxWidget和vtkExtractVOI实现剪裁。之前介绍过vtkBoxWidget,主要是说基本功能和接口,没有贴出完整实例。
      Study-VTK:vtkWidget 分割/配准类之 正交六面体3D小部件 vtkBoxWidget
      干脆直接介绍如何利用vtkwidget 实现剪裁。本文代码基本就是 cp 一遍vtk相关接口。


    1 利用vtkwidget 剪裁步骤

    实现剪裁需要三步:

    1. 输入被剪裁模型;
        被剪裁数据一般有:
          影像(vti/vtr格式 dcm/图片/矩阵 等数据)、
          模型(vtp/vtu格式 stl/obj/体网格 等数据)、
          场景

    2. 剪切区域选择(交互);
        vtk提供大量现成的交互vtkWidget。可以自己开发交互,我只搞过一个,很麻烦。现成的虽然丑点但是是很多前辈积累的,比较实用。一般不建议自己开发。跟剪切相关的,遵循kiss原则,大家直接想到的无非就是:
          平面上任意点两个点,模型分成两个部分
          平面上任意点多个点,练成闭合样条曲线,分成曲线内、曲线外
          空间放置一个球,分成球内求外
          空间放置一个立方体,截取立方体内外

    3. 剪切算法实现。
          模型的基本上是自己搞一个无限长的平面,利用在利用现成的与或非
          影像的话vtkExtractVOI可以实现任意剪裁

    2 利用vtkwidget 剪裁模型展示

      临时搞一个demo,可以先看下效果。

      被剪裁模型:

    1. 本地stl文件

      剪切区域选择:

    1. 球、
    2. box、
    3. 平面任意两点、
    4. 平面任意多点

      剪切算法:

    1. vtkClipPolyData实现两个模型取并、
    2. vtkClipPolyData实现两个模型取异、
    3. vtkCutter实现两个模型去交线、
    4. vtkBoxClipDataSet生成无限平面后去并

    stl、box、取交集
    在这里插入图片描述
    stl、box、取外部
    在这里插入图片描述
    stl、box、取交线
    在这里插入图片描述
    stl、ball、取交集
    在这里插入图片描述

    stl、ball、取外部
    在这里插入图片描述

    stl、平面直线、取最大连通域(直线设置比较细,录屏压缩后看不到了)
    在这里插入图片描述

    stl、平面多点、取最大连通域(顺时针内、逆时针外)
    在这里插入图片描述

    3 利用vtkwidget 剪裁模型代码

      vtkWidget 大部分使用方法基本一样,vtkBoxWidget举例

    1 初始化

     if (widget_type_ == BOX) {
            if (clip_box_widget_ == nullptr) {
                clip_box_widget_ = vtkSmartPointer<vtkBoxWidget>::New();
                clip_box_widget_->GetFaceProperty()->SetColor(0.6, 0.6, 0.2);
                clip_box_widget_->GetFaceProperty()->SetOpacity(0.25);
                clip_box_widget_->SetInteractor(
                    vmtk_renderer_->GetRenderWindowInteractor());
            }
        }
    

    1 设置rans

      clip_box_widget_->GetTransform(transform_);
    

    2 开启交互

        if (widget_type_ == BOX) {
            clip_box_widget_->SetInputData(surface_);
            clip_box_widget_->PlaceWidget();
        } else if (widget_type_ == SPHERE) {
        } else if (widget_type_ == LINE) {
        }
        if (transform_ && widget_type_ == BOX) {
            clip_box_widget_->SetTransform(transform_);
            clip_box_widget_->On();
        }
    

    3 交互完成后获取数据

    vtkSmartPointer<vtkClipPolyData> clipper = vtkSmartPointer<vtkClipPolyData>::New();
        vtkSmartPointer<vtkPlanes> clip_planes_function;
        vtkSmartPointer<vtkSphere> clip_sphere_function;
        clipper->SetInputData(surface_);
        clipper->GenerateClippedOutputOn();
        clipper->SetInsideOut(inside_out_);
        if (widget_type_ == BOX) {
            clip_planes_function = vtkSmartPointer<vtkPlanes>::New();
            clipper->SetClipFunction(clip_planes_function);
        } else if (widget_type_ == SPHERE) {
            clip_sphere_function = vtkSmartPointer<vtkSphere>::New();
            clipper->SetClipFunction(clip_sphere_function);
        }
        vtkSmartPointer<vtkCutter> cutter = vtkSmartPointer<vtkCutter>::New();
        cutter->SetInputData(surface_);
        if (widget_type_ == BOX) {
            cutter->SetCutFunction(clip_planes_function);
        } else if (widget_type_ == SPHERE) {
            cutter->SetCutFunction(clip_sphere_function);
        }
        clipped_surface_ = vtkSmartPointer<vtkPolyData>::New();
        cut_lines_ = vtkSmartPointer<vtkPolyData>::New();
        if (widget_type_ == BOX) {
            clip_box_widget_->GetPlanes(clip_planes_function);
        } else if (widget_type_ == SPHERE) {
            clip_sphere_widget_->GetSphere(clip_sphere_function);
        }
        clipper->Update();
        if (widget_type_ == BOX) {
            clip_box_widget_->Off();
        } else if (widget_type_ == SPHERE) {
            clip_sphere_widget_->Off();
        }
        cutter->Update();
    

    4 关闭交互

        if (widget_type_ == BOX) {
            clip_box_widget_->Off();
        } else if (widget_type_ == SPHERE) {
            clip_sphere_widget_->Off();
        }
    



    完整代码

      我这里把vtkSeedWidget、vtkBoxWidget、vtkSphereWidget封装在一起。

    #ifndef VMTKSURFACECLIPPER_H
    #define VMTKSURFACECLIPPER_H
    
    #include <QObject>
    #include <QPointer>
    #include "vmtkrenderer.h"
    
    #include <vtkPolyData.h>
    #include <vtkTransform.h>
    #include <vtkBoxWidget.h>
    #include <vtkSeedWidget.h>
    #include <vtkSmartPointer.h>
    #include <vtkSphereWidget.h>
    
    
    
    class SurfaceClipper : public QObject {
        Q_OBJECT
      public:
        enum WidgetType {// 切割方式
            BOX,
            SPHERE,
            LINE
        };
      public:
        explicit SurfaceClipper(QObject *parent = nullptr);
        virtual ~SurfaceClipper() override;
        void Execute();
        void SetEnable(const bool value);
        void SetSurface(const vtkSmartPointer<vtkPolyData> value);
        void SetWidgetType(const WidgetType value);
        void SetTransform(const vtkSmartPointer<vtkTransform> value);
        void SetInsideOut(const bool value);
        void SetVmtkRenderer(const QPointer<VmtkRenderer> value);
        vtkSmartPointer<vtkPolyData> GetSurface() const;
        vtkSmartPointer<vtkPolyData> GetClippedSurface() const;
        vtkSmartPointer<vtkPolyData> GetCutLines() const;
        vtkSmartPointer<vtkTransform> GetTransForm() const;
      Q_SIGNALS:
        void SignalClippedFinish();
      private:
        void Initial();
        void Display();
        void ClipCallback();
      private Q_SLOTS:
        void SlotKeyPressed(const QString &key);
        void SlotSeedChanged(vtkObject *caller, unsigned long vtk_event,
                             void *client_data, void *call_data);
      private:
        bool first_connect_;
        bool own_renderer_;
        bool inside_out_;
    
        QPointer<VmtkRenderer> vmtk_renderer_;
    
        vtkSmartPointer<vtkPolyData> surface_;
        vtkSmartPointer<vtkPolyData> clipped_surface_;
        vtkSmartPointer<vtkPolyData> cut_lines_;
    
        vtkSmartPointer<vtkEventQtSlotConnect> vtk_connections_;
        vtkSmartPointer<vtkSeedWidget> seed_widget_;
        vtkSmartPointer<vtkBoxWidget> clip_box_widget_;
        vtkSmartPointer<vtkSphereWidget> clip_sphere_widget_;
        vtkSmartPointer<vtkTransform> transform_;
    
        WidgetType widget_type_;
    };
    
    #endif // VMTKSURFACECLIPPER_H
    
    
    #include "vmtksurfaceclipper.h"
    #include <QDebug>
    #include <vtkPoints.h>
    #include <vtkCamera.h>
    #include <vtkCutter.h>
    #include <vtkPlanes.h>
    #include <vtkSphere.h>
    
    #include <vtkStripper.h>
    #include <vtkProperty.h>
    #include <vtkPointData.h>
    #include <vtkProperty2D.h>
    #include <vtkClipPolyData.h>
    #include <vtkContourFilter.h>
    #include <vtkCleanPolyData.h>
    #include <vtkPolyDataMapper.h>
    #include <vtkBoxClipDataSet.h>
    #include <vtkSeedRepresentation.h>
    #include <vtkDataSetSurfaceFilter.h>
    #include <vtkPolyDataConnectivityFilter.h>
    #include <vtkPointHandleRepresentation2D.h>
    
    
    SurfaceClipper::SurfaceClipper(QObject *parent) : QObject(parent) {
        Initial();
    }
    
    SurfaceClipper::~SurfaceClipper() {
        if (own_renderer_) {
            vmtk_renderer_->deleteLater();
        }
    }
    
    /**
     * @brief SurfaceClipper::Execute
     * ui交互开启
     */
    void SurfaceClipper::Execute() {
        qDebug();
        if (surface_ == nullptr) {
            qWarning() << "no Surface";
            return ;
        }
        if (vmtk_renderer_ == nullptr) {
            vmtk_renderer_ = new VmtkRenderer();
            vmtk_renderer_->Initialize();
            own_renderer_ = true;
        }
        if (widget_type_ == BOX) {
            if (clip_box_widget_ == nullptr) {
                clip_box_widget_ = vtkSmartPointer<vtkBoxWidget>::New();
                clip_box_widget_->GetFaceProperty()->SetColor(0.6, 0.6, 0.2);
                clip_box_widget_->GetFaceProperty()->SetOpacity(0.25);
                clip_box_widget_->SetInteractor(
                    vmtk_renderer_->GetRenderWindowInteractor());
            }
        } else if (widget_type_ == SPHERE) {
            if (clip_sphere_widget_ == nullptr) {
                clip_sphere_widget_ = vtkSmartPointer<vtkSphereWidget>::New();
                clip_sphere_widget_->GetSphereProperty()->SetColor(0.6, 0.6, 0.2);
                clip_sphere_widget_->GetSphereProperty()->SetOpacity(0.25);
                clip_sphere_widget_->GetSelectedSphereProperty()
                ->SetColor(0.6, 0.0, 0.0);
                clip_sphere_widget_->GetSelectedSphereProperty()
                ->SetOpacity(0.75);
                clip_sphere_widget_->SetRepresentationToSurface();
                clip_sphere_widget_->SetPhiResolution(20);
                clip_sphere_widget_->SetThetaResolution(20);
                clip_sphere_widget_->SetInteractor(
                    vmtk_renderer_->GetRenderWindowInteractor());
            }
        } else if (widget_type_ == LINE) {
            if (this->seed_widget_ == nullptr) {
                this->seed_widget_ = vtkSmartPointer<vtkSeedWidget>::New();
                vtkNew<vtkPointHandleRepresentation2D> handle_rep;
                handle_rep->GetProperty()->SetColor(1, 0, 0);
                vtkNew<vtkSeedRepresentation> widget_rep;
                widget_rep->SetHandleRepresentation(handle_rep);
                this->seed_widget_->SetInteractor(
                    this->vmtk_renderer_->GetRenderWindowInteractor());
                this->seed_widget_->SetRepresentation(widget_rep);
            }
            qint32 num_seeds =
                this->seed_widget_->GetSeedRepresentation()->GetNumberOfSeeds();
            for (qint32 i = 0; i < num_seeds; ++i) {
                this->seed_widget_->GetSeedRepresentation()->RemoveLastHandle();
                this->seed_widget_->DeleteSeed(
                    this->seed_widget_->GetSeedRepresentation()->GetNumberOfSeeds());
            }
        }
        if (first_connect_) {
            connect(vmtk_renderer_, &VmtkRenderer::SignalKeyPressed,
                    this, &SurfaceClipper::SlotKeyPressed);
            this->vtk_connections_ = vtkSmartPointer<vtkEventQtSlotConnect>::New();
            vtk_connections_->Connect(seed_widget_, vtkCommand::PlacePointEvent,
                                      this, SLOT(SlotSeedChanged(vtkObject *, unsigned long,
                                                 void *, void *)));
            first_connect_ = false;
        }
        transform_ = vtkSmartPointer<vtkTransform>::New();
        if (widget_type_ == BOX) {
            clip_box_widget_->GetTransform(transform_);
        }
        Display();
        if (own_renderer_) {
            vmtk_renderer_->Deallocate();
        }
    }
    
    /**
     * @brief SurfaceClipper::SetSurface
     * 设置输入模型
     * @param value
     */
    void SurfaceClipper::SetSurface(const vtkSmartPointer<vtkPolyData> value) {
        surface_ = value;
    }
    
    /**
     * @brief SurfaceClipper::SetWidgetType
     * 设置切割方式
     * @param value
     */
    void SurfaceClipper::SetWidgetType(const SurfaceClipper::WidgetType value) {
        widget_type_ = value;
    }
    
    /**
     * @brief SurfaceClipper::SetTransform
     * 设置Trans转换
     * @param value
     */
    void SurfaceClipper::SetTransform(const vtkSmartPointer<vtkTransform> value) {
        transform_ = value;
    }
    
    /**
     * @brief SurfaceClipper::SetInsideOut
     * 设置是否取反
     * @param value
     */
    void SurfaceClipper::SetInsideOut(const bool value) {
        inside_out_ = value;
    }
    
    /**
     * @brief SurfaceClipper::SetVmtkRenderer
     * 输入Renderer
     * @param value
     */
    void SurfaceClipper::SetVmtkRenderer(const QPointer<VmtkRenderer> value) {
        vmtk_renderer_ = value;
    }
    
    /**
     * @brief SurfaceClipper::GetSurface
     * 获取剪切后模型(交)
     * @return
     */
    vtkSmartPointer<vtkPolyData> SurfaceClipper::GetSurface() const {
        return surface_;
    }
    
    /**
     * @brief SurfaceClipper::GetClippedSurface
     * 获取剪切后模型(异)
     * @return
     */
    vtkSmartPointer<vtkPolyData> SurfaceClipper::GetClippedSurface() const {
        return clipped_surface_;
    }
    
    /**
     * @brief SurfaceClipper::GetCutLines
     * 获取剪切后交线
     * @return
     */
    vtkSmartPointer<vtkPolyData> SurfaceClipper::GetCutLines() const {
        return cut_lines_;
    }
    
    /**
     * @brief SurfaceClipper::GetTransForm
     * BOX模式剪切转换
     * @return
     */
    vtkSmartPointer<vtkTransform> SurfaceClipper::GetTransForm() const {
        return transform_;
    }
    
    /**
     * @brief SurfaceClipper::Initial
     * 初始化
     */
    void SurfaceClipper::Initial() {
        surface_ = nullptr;
        clipped_surface_ = nullptr;
        cut_lines_ = nullptr;
        vmtk_renderer_ = nullptr;
        clip_box_widget_ = nullptr;
        clip_sphere_widget_ = nullptr;
        first_connect_ = true;
        own_renderer_ = false;
        transform_ = nullptr;
        widget_type_ = LINE;
        inside_out_ = true;
        setObjectName("vmtksurfaceclipper");
    }
    
    /**
     * @brief SurfaceClipper::Display
     * 开启widget
     */
    void SurfaceClipper::Display() {
        if (widget_type_ == BOX) {
            clip_box_widget_->SetInputData(surface_);
            clip_box_widget_->PlaceWidget();
        } else if (widget_type_ == SPHERE) {
            clip_sphere_widget_->SetInputData(surface_);
            clip_sphere_widget_->PlaceWidget();
            clip_sphere_widget_->On();
        } else if (widget_type_ == LINE) {
            this->seed_widget_->On();
        }
        if (transform_ && widget_type_ == BOX) {
            clip_box_widget_->SetTransform(transform_);
            clip_box_widget_->On();
        }
        vmtk_renderer_->Render();
    }
    
    /**
     * @brief SurfaceClipper::ClipCallback
     * 剪裁函数
     */
    void SurfaceClipper::ClipCallback() {
        qDebug();
        if ((widget_type_ == BOX &&
                clip_box_widget_->GetEnabled() != 1) ||
                (widget_type_ == SPHERE &&
                 clip_sphere_widget_->GetEnabled() != 1)) {
            return ;
        }
        if(widget_type_ == LINE) {
            if (this->seed_widget_->GetSeedRepresentation()->GetNumberOfSeeds() != 2
                    || this->surface_ == nullptr) {
                return ;
            }
            double pos1[3], pos2[3];
            this->seed_widget_->GetSeedRepresentation()->GetSeedWorldPosition(0, pos1);
            this->seed_widget_->GetSeedRepresentation()->GetSeedWorldPosition(1, pos2);
            this->seed_widget_->Off();
            QList<QList<double>> pts = {
                {0, 0, 0},
                {1, 0, 0},
                {1, 1, 0},
                {0, 1, 0},
                {0, 0, 1},
                {1, 0, 1},
                {1, 1, 1},
                {0, 1, 1}
            };
            vtkNew<vtkPoints> points;
            for (qint32 i = 0; i < pts.size(); ++i) {
                points->InsertPoint(i, pts[i][0], pts[i][1], pts[i][2]);
            }
            double direction[3];
            this->vmtk_renderer_->GetRenderer()->GetActiveCamera()
            ->GetDirectionOfProjection(direction);
            points->SetPoint(0,
                             pos1[0] - direction[0] * 1000,
                             pos1[1] - direction[1] * 1000,
                             pos1[2] - direction[2] * 1000);
            points->SetPoint(3,
                             pos1[0] + direction[0] * 1000,
                             pos1[1] + direction[1] * 1000,
                             pos1[2] + direction[2] * 1000);
            points->SetPoint(1,
                             pos2[0] - direction[0] * 1000,
                             pos2[1] - direction[1] * 1000,
                             pos2[2] - direction[2] * 1000);
            points->SetPoint(2,
                             pos2[0] + direction[0] * 1000,
                             pos2[1] + direction[1] * 1000,
                             pos2[2] + direction[2] * 1000);
            double direction2[3], direction_offset[3];
            direction2[0] = pos1[0] - pos2[0];
            direction2[1] = pos1[1] - pos2[1];
            direction2[2] = pos1[2] - pos2[2];
            vtkMath::Cross(direction, direction2, direction_offset);
            points->SetPoint(4,
                             pos1[0] - direction[0] * 1000 + direction_offset[0] * 0.01,
                             pos1[1] - direction[1] * 1000 + direction_offset[1] * 0.01,
                             pos1[2] - direction[2] * 1000 + direction_offset[2] * 0.01);
            points->SetPoint(7,
                             pos1[0] + direction[0] * 1000 + direction_offset[0] * 0.01,
                             pos1[1] + direction[1] * 1000 + direction_offset[1] * 0.01,
                             pos1[2] + direction[2] * 1000 + direction_offset[2] * 0.01);
            points->SetPoint(5,
                             pos2[0] - direction[0] * 1000 + direction_offset[0] * 0.01,
                             pos2[1] - direction[1] * 1000 + direction_offset[1] * 0.01,
                             pos2[2] - direction[2] * 1000 + direction_offset[2] * 0.01);
            points->SetPoint(6,
                             pos2[0] + direction[0] * 1000 + direction_offset[0] * 0.01,
                             pos2[1] + direction[1] * 1000 + direction_offset[1] * 0.01,
                             pos2[2] + direction[2] * 1000 + direction_offset[2] * 0.01);
            vtkNew<vtkBoxClipDataSet> box_clip;
            box_clip->SetInputData(this->surface_);
            box_clip->GenerateClippedOutputOn();
            double n0[3], n1[3], n2[3], n3[3], n4[3], n5[3];
            double p0[3], p1[3], p2[3], p3[3], p4[3], p5[3];
            double pt0[3], pt1[3], pt2[3], pt3[3], pt4[3], pt5[3], pt6[3], pt7[3];
            points->GetPoint(0, pt0);
            points->GetPoint(1, pt1);
            points->GetPoint(2, pt2);
            points->GetPoint(3, pt3);
            points->GetPoint(4, pt4);
            points->GetPoint(5, pt5);
            points->GetPoint(6, pt6);
            points->GetPoint(7, pt7);
            for (qint32 i = 0; i < 3; ++i) {
                p0[i] = (pt1[i] + pt3[i]) / 2;
                n0[i] = (pt1[i] - pt5[i]);
                p1[i] = (pt5[i] + pt7[i]) / 2;
                n1[i] = (pt5[i] - pt1[i]);
                p2[i] = (pt2[i] + pt5[i]) / 2;
                n2[i] = (pt5[i] - pt4[i]);
                p3[i] = (pt3[i] + pt4[i]) / 2;
                n3[i] = (pt4[i] - pt5[i]);
                p4[i] = (pt0[i] + pt5[i]) / 2;
                n4[i] = (pt5[i] - pt6[i]);
                p5[i] = (pt2[i] + pt7[i]) / 2;
                n5[i] = (pt6[i] - pt5[i]);
            }
            box_clip->SetBoxClip(n0, p0,
                                 n1, p1,
                                 n2, p2,
                                 n3, p3,
                                 n4, p4,
                                 n5, p5);
            vtkNew<vtkDataSetSurfaceFilter> surface_in;
            surface_in->SetInputConnection(box_clip->GetOutputPort(0));
            vtkNew<vtkDataSetSurfaceFilter> surface_out;
            surface_out->SetInputConnection(box_clip->GetOutputPort(1));
            vtkNew<vtkPolyDataConnectivityFilter> connectivity_filter;
            connectivity_filter->SetInputConnection(surface_out->GetOutputPort());
            connectivity_filter->SetExtractionModeToLargestRegion();
            connectivity_filter->Update();
            this->surface_ = connectivity_filter->GetOutput();
            return;
        }
        vtkSmartPointer<vtkClipPolyData> clipper = vtkSmartPointer<vtkClipPolyData>::New();
        vtkSmartPointer<vtkPlanes> clip_planes_function;
        vtkSmartPointer<vtkSphere> clip_sphere_function;
        clipper->SetInputData(surface_);
        clipper->GenerateClippedOutputOn();
        clipper->SetInsideOut(inside_out_);
        if (widget_type_ == BOX) {
            clip_planes_function = vtkSmartPointer<vtkPlanes>::New();
            clipper->SetClipFunction(clip_planes_function);
        } else if (widget_type_ == SPHERE) {
            clip_sphere_function = vtkSmartPointer<vtkSphere>::New();
            clipper->SetClipFunction(clip_sphere_function);
        }
        vtkSmartPointer<vtkCutter> cutter = vtkSmartPointer<vtkCutter>::New();
        cutter->SetInputData(surface_);
        if (widget_type_ == BOX) {
            cutter->SetCutFunction(clip_planes_function);
        } else if (widget_type_ == SPHERE) {
            cutter->SetCutFunction(clip_sphere_function);
        }
        clipped_surface_ = vtkSmartPointer<vtkPolyData>::New();
        cut_lines_ = vtkSmartPointer<vtkPolyData>::New();
        if (widget_type_ == BOX) {
            clip_box_widget_->GetPlanes(clip_planes_function);
        } else if (widget_type_ == SPHERE) {
            clip_sphere_widget_->GetSphere(clip_sphere_function);
        }
        clipper->Update();
        if (widget_type_ == BOX) {
            clip_box_widget_->Off();
        } else if (widget_type_ == SPHERE) {
            clip_sphere_widget_->Off();
        }
        cutter->Update();
        cut_lines_->DeepCopy(cutter->GetOutput());
        surface_->DeepCopy(clipper->GetOutput());
        clipped_surface_->DeepCopy(clipper->GetClippedOutput());
    }
    
    /**
     * @brief SurfaceClipper::SlotKeyPressed
     * 键盘回调函数
     * @param key
     */
    void SurfaceClipper::SlotKeyPressed(const QString &key) {
        if (key == "space") {
            ClipCallback();
            emit SignalClippedFinish();
        } else if (key == "Escape") {
            if (widget_type_ == BOX) {
                clip_box_widget_->Off();
            } else if (widget_type_ == SPHERE) {
                clip_sphere_widget_->Off();
            } else if (widget_type_ == SPHERE) {
                seed_widget_->Off();
            }
            emit SignalClippedFinish();
        }
    }
    
    /**
     * @brief SurfaceClipper::SetEnable
     * 设置是否启动
     * @param value
     */
    void SurfaceClipper::SetEnable(const bool value) {
        if (widget_type_ == BOX) {
            clip_box_widget_->SetEnabled(value);
        } else if (widget_type_ == SPHERE) {
            clip_sphere_widget_->SetEnabled(value);
        }
    }
    
    /**
     * @brief SurfaceClipper::SlotSeedChanged
     * 点选取后槽函数
     * @param caller
     * @param vtk_event
     * @param client_data
     * @param call_data
     */
    void SurfaceClipper::SlotSeedChanged(
        vtkObject *caller, unsigned long vtk_event,
        void *client_data, void *call_data) {
        Q_UNUSED(client_data)
        if (vtk_event == vtkCommand::PlacePointEvent) {
            qint32 n = *static_cast<int *>(call_data);
            vtkSmartPointer<vtkSeedWidget> widget = dynamic_cast<vtkSeedWidget *>(caller);
            if (n >= 0 && widget) {
                qint32 num_seeds = widget->GetSeedRepresentation()->GetNumberOfSeeds();
                if (num_seeds > 2) {
                    this->seed_widget_->DeleteSeed(0);
                }
            }
        }
    }
    
    
    

    4 利用vtkwidget 剪裁影像展示

      影像剪裁一般在平面选择切割方向让后利用vtkExtractVOI重新切割影像,网友问vtkExtractVOI + vtkBoxWidget应该是在空间剪裁。方法跟上边模型切割一样,周末不上边了补上。

    在这里插入图片描述

    在这里插入图片描述

    5 利用vtkwidget 剪裁影像代码

    Study-VTK:三维影像实现任意方向、大小的切割

    展开全文
  • 本文介绍了Flutter应用程序中Widget,State,BuildContext和InheritedWidget的重要概念。 特别注意InheritedWidget,它是最重要且记录较少的小部件之一。 难度:初学者 前言 Flutter中Widget,State和BuildContext的...
  • Android的widget使用listview布局

    热门讨论 2013-04-19 17:04:55
    Android的widget使用listview布局,快速上手,可以直接用在项目里面
  • jquery.ui.widget.js

    2014-12-05 11:45:21
    jquery的ui插件核心js文件: jquery.ui.widget.js
  • 提出问题 用Flutter写界面写了一段时间了,感觉很爽,尤其是热...state 里面为啥可以直接获取到 widget 对象? build 方法是在什么时候调用的? BuildContext 是什么? Widget 频繁更改创建是否会影响性能?复用

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 492,565
精华内容 197,026
关键字:

widget

友情链接: SNT_MAX.rar