03_Flutter自定义下拉菜单

news2024/10/1 1:22:01

03_Flutter自定义下拉菜单

在Flutter的内置api中,可以使用showMenu实现类似下拉菜单的效果,或者使用PopupMenuButton组件,PopupMenuButton内部也是使用了showMenu这个api,但是使用showMenu时,下拉面板的显示已经被约定死了,只能放一个简单的列表,没有办法定制下来面板的ui,并且下拉面板的宽高需要通过指定constraints进行限制,下面是一个简单的showMenu的用法:

Container(
  height: 44,
  margin: EdgeInsetsDirectional.only(top: 30, start: 30, end: 30),
  color: Colors.red,
  child: Builder(
    builder: (context) {
      return GestureDetector(
        onTap: () {
          final RenderBox button = context.findRenderObject()! as RenderBox;
          final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;

          Offset offset = Offset(0.0, button.size.height);

          RelativeRect position = RelativeRect.fromRect(
            Rect.fromPoints(
              button.localToGlobal(offset, ancestor: overlay),
              button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay),
            ),
            Offset.zero & overlay.size,
          );
          
          showMenu(
            context: context,
            position: position,
            constraints: BoxConstraints(maxWidth: 315, maxHeight: 200),
            items: List.generate(5, (index) => PopupMenuItem(
              child: Container(
                width: 375,
                height: 44,
                alignment: AlignmentDirectional.center,
                child: Text("item"),
              )
            ))
          );
        },
      );
    },
  ),
)

在这里插入图片描述

接下来,我们将参照showMenu的源码,依葫芦画个瓢,自定义一个下拉菜单的api,并可自由定制下拉面板的布局内容,篇幅有点长,请耐心观看。

一.确定下拉面板的起始位置

查看PopupMenuButton的源码,可以知道,PopupMenuButton在确定下拉面板的起始位置时,是先获取下拉面板依赖的按钮的边界位置和整个页面的显示区域边界,通过这两个边界计算得到一个RelativeRect,这个RelativeRect就是用来描述下拉面板的起始位置的。

showPopup(BuildContext context) {
  final RenderBox button = context.findRenderObject()! as RenderBox;
  final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;

  Offset offset = Offset(0.0, button.size.height);

  RelativeRect position = RelativeRect.fromRect(
    Rect.fromPoints(
      button.localToGlobal(offset, ancestor: overlay),
      button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay),
    ),
    Offset.zero & overlay.size,
  );
}

注:上述代码中用的的context对象,必须是下拉面板依赖的按钮对应的context,否则最后计算出来的RelativeRect是不对的。计算过程不做过多解释了,直接上图:

在这里插入图片描述

二.确定下拉面板的布局约束

  • 水平方向确定最大宽度,比较简单,下拉面板的最大宽度和它所依赖的按钮的宽度一致即可
  • 垂直方向上的最大高度,上一步已经确定了position的值,垂直方向上的最大高度可以取position.top - buttonHeight - padding.top - kToolbarHeight和constraints.biggest.height - position.top - padding.bottom的最大值,padding为安全区域的大小
  • 使用CustomSingleChildLayout作为下拉面板的父容器,并实现一个SingleChildLayoutDelegate,重写getConstraintsForChild,确定约束
EdgeInsets padding = MediaQuery.paddingOf(context);

class _CustomPopupRouteLayout extends SingleChildLayoutDelegate {

  final RelativeRect position;

  _CustomPopupRouteLayout(this.position);

  
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    Size buttonSize = position.toSize(constraints.biggest);

    double constraintsWidth = buttonSize.width;
    double constraintsHeight = max(position.top - buttonSize.height - padding.top - kToolbarHeight, constraints.biggest.height - position.top - padding.bottom);

    return BoxConstraints.loose(Size(constraintsWidth, constraintsHeight));
  }

  
  bool shouldRelayout(covariant _CustomPopupRouteLayout oldDelegate) {
    return position != oldDelegate.position;
  }
}

三.显示下拉面板

我们先把下拉面板显示出来看看效果,这里的下拉面板其实是一个弹出层,而在Flutter中,所有的弹出层的显示和页面路由是一样的,都是通过Navigator.push进行显示,参照showMenu的源码,这里的弹出层我们让其继承PopupRoute

class _CustomPopupRoute<T> extends PopupRoute<T> {
  final RelativeRect position;
  
  final String? barrierLabel;

  _CustomPopupRoute({
    required this.position,
    required this.barrierLabel,
  });

  
  Color? get barrierColor => null;

  
  bool get barrierDismissible => true;

  
  Duration get transitionDuration => Duration(milliseconds: 200);

  
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
    return CustomSingleChildLayout(
      delegate: _CustomPopupRouteLayout(position),
      child: Material(
        child: Container(
          color: Colors.yellow,
          width: double.infinity,
          height: double.infinity,
          alignment: AlignmentDirectional.center,
          child: Text("popup content"),
        ),
      ),
    );
  }

}
showPopup(BuildContext context) {
  final RenderBox button = context.findRenderObject()! as RenderBox;
  final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;

  Offset offset = Offset(0.0, button.size.height);

  RelativeRect position = RelativeRect.fromRect(
    Rect.fromPoints(
      button.localToGlobal(offset, ancestor: overlay),
      button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay),
    ),
    Offset.zero & overlay.size,
  );
  
  Navigator.of(context).push(_CustomPopupRoute(
      position: position, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel
  ));
}

在这里插入图片描述

如图,黄色区域就是下拉面板,可以看到,点击按钮下拉面板显示,点击下拉面板以外的区域,下拉面板关闭,但是位置好像不对,因为我们根本就没去确定下拉面板的位置。

四.确定下拉面板的位置


Offset getPositionForChild(Size size, Size childSize) {
	return super.getPositionForChild(size, childSize);
}

只需要重写SingleChildLayoutDelegate的getPositionForChild方法,返回一个Offset对象,Offset的x、y的值就代表下拉面板左上角的位置,那么问题来了,x、y的值怎么确定?

  • 确定x

    x = position.left

  • 确定y

    • position.top + constraintsHeight > size.height - paddingBottom

    在这里插入图片描述

    • position.top + constraintsHeight <= size.height - paddingBottom

    在这里插入图片描述

EdgeInsets padding = MediaQuery.paddingOf(context);

class _CustomPopupRouteLayout extends SingleChildLayoutDelegate {

  final RelativeRect position;
  EdgeInsets padding;

  _CustomPopupRouteLayout(this.position, this.padding);

  
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    Size buttonSize = position.toSize(constraints.biggest);

    double constraintsWidth = buttonSize.width;
    double constraintsHeight = max(position.top - buttonSize.height - padding.top - kToolbarHeight, constraints.biggest.height - position.top - padding.bottom);

    return BoxConstraints.loose(Size(constraintsWidth, constraintsHeight));
  }

  
    
  Offset getPositionForChild(Size size, Size childSize) {
    double x = position.left;
    double y = position.top;
    final double buttonHeight = size.height - position.top - position.bottom;
    double constraintsHeight = max(position.top - buttonHeight - padding.top - kToolbarHeight, size.height - position.top - padding.bottom);
    if(position.top + constraintsHeight > size.height - padding.bottom) {
      y = position.top - childSize.height - buttonHeight;
    }

    return Offset(x, y);
  }
  

  
  bool shouldRelayout(covariant _CustomPopupRouteLayout oldDelegate) {
    return position != oldDelegate.position || padding != oldDelegate.padding;
  }
}

六.下拉动画实现

创建动画插值器,其值从0 ~ 1之间变化,动画时长为PopupRoute中重写的transitionDuration,及200ms时间内,从0变到1,或者从1变到0

final CurveTween heightFactorTween = CurveTween(curve: const Interval(0.0, 1.0));

使用AnimatedBuilder改造PopupRoute的布局结构,根据heightFactorTween的动画执行值 * 下拉菜单内容容器的高度,改变拉菜单内容的高度即可,这里暂时将高度设置为固定值300。

class _CustomPopupRoute<T> extends PopupRoute<T> {
  
  ...
  
  
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
    EdgeInsets padding = MediaQuery.paddingOf(context);
    final CurveTween heightFactorTween = CurveTween(curve: const Interval(0.0, 1.0));
    return MediaQuery.removePadding(
      context: context,
      removeTop: true,
      removeBottom: true,
      removeLeft: true,
      removeRight: true,
      child: CustomSingleChildLayout(
        delegate: _CustomPopupRouteLayout(position, padding),
        child: AnimatedBuilder(
          animation: animation,
          builder: (context, child) {
            return Material(
              child: Container(
                height: 300*heightFactorTween.evaluate(animation),
                child: child,
              )
            );
          },
          child: Container(
            color: Colors.yellow,
            width: double.infinity,
            height: 300,
            alignment: AlignmentDirectional.center,
            child: Text("popup content"),
          ),
        ),
      ),
    );
  }
}

下拉动画效果已经出来了,但是实际情况下,下拉面板的高度是不能直接在组件层固定写死的,所以这里需要动态计算出下拉面板的高度。

七.下拉面板动态高度,支持下拉动画

想要获取组件的高度,需要等到组件的layout完成后,才能获取到组件的大小,因此,我们需要自定义一个RenderObject,重写其performLayout,在子控件第一次layout完后,获取到子控件的初始高度,子控件的初始化高度结合动画的高度比例系数来最终确定自身的大小。

class _RenderHeightFactorBox extends RenderShiftedBox {
  double _heightFactor;
  _RenderHeightFactorBox({
    RenderBox? child,
    double? heightFactor,
  }):_heightFactor = heightFactor ?? 1.0, super(child);

  double get heightFactor => _heightFactor;

  set heightFactor(double value) {
    if (_heightFactor == value) {
      return;
    }
    _heightFactor = value;
    markNeedsLayout();
  }

  
  void performLayout() {
    final BoxConstraints constraints = this.constraints;

    if (child == null) {
      size = constraints.constrain(Size.zero);
      return;
    }

    child!.layout(constraints, parentUsesSize: true);

    size = constraints.constrain(Size(
      child!.size.width,
      child!.size.height,
    ));

    child!.layout(constraints.copyWith(maxWidth: size.width, maxHeight: size.height * heightFactor), parentUsesSize: true);

    size = constraints.constrain(Size(
      child!.size.width,
      child!.size.height,
    ));
  }
}

接着定义一个SingleChildRenderObjectWidget,并引用_RenderHeightFactorBox

class _HeightFactorBox extends SingleChildRenderObjectWidget {
  final double? heightFactor;

  const _HeightFactorBox({
    super.key,
    this.heightFactor,
    super.child,
  });

  
  RenderObject createRenderObject(BuildContext context) => _RenderHeightFactorBox(heightFactor: heightFactor);

  
  void updateRenderObject(BuildContext context, _RenderHeightFactorBox renderObject) {
    renderObject.heightFactor = heightFactor ?? 1.0;
  }
}

最后把下拉面板中,执行动画的child使用_HeightFactorBox包裹,并传入heightFactorTween的执行结果即可。


Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
  EdgeInsets padding = MediaQuery.paddingOf(context);
  final CurveTween heightFactorTween = CurveTween(curve: const Interval(0.0, 1.0));
  return MediaQuery.removePadding(
    context: context,
    removeTop: true,
    removeBottom: true,
    removeLeft: true,
    removeRight: true,
    child: CustomSingleChildLayout(
      delegate: _CustomPopupRouteLayout(position, padding),
      child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return Material(
            child: _HeightFactorBox(
              heightFactor: heightFactorTween.evaluate(animation),
              child: child,
            )
          );
        },
        child: Container(
          color: Colors.yellow,
          width: double.infinity,
          height: double.infinity,
          alignment: AlignmentDirectional.center,
          child: Text("popup content"),
        ),
      ),
    ),
  );
}

八.完整代码

class TestPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("下拉菜单"),
        backgroundColor: Colors.blue,
      ),
      body: Container(
        width: 375,
        child: Column(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Container(
              height: 44,
              margin: const EdgeInsetsDirectional.only(top: 30, start: 30, end: 30),
              color: Colors.red,
              child: Builder(
                builder: (context) {
                  return GestureDetector(
                    onTap: () {
                      showPopup(context: context, builder: (context) {
                        return Container(
                          height: 400,
                          decoration: const BoxDecoration(
                            color: Colors.yellow
                          ),
                          child: SingleChildScrollView(
                            physics: const ClampingScrollPhysics(),
                            child: Column(
                              mainAxisSize: MainAxisSize.max,
                              mainAxisAlignment: MainAxisAlignment.center,
                              crossAxisAlignment: CrossAxisAlignment.stretch,
                              children: List<Widget>.generate(29, (index) {
                                int itemIndex = index ~/ 2;
                                if(index.isEven) {
                                  return Container(
                                    height: 44,
                                    alignment: AlignmentDirectional.center,
                                    child: Text("item$itemIndex"),
                                  );
                                } else {
                                  return Container(
                                    height: 1,
                                    color: Colors.grey,
                                  );
                                }
                              }),
                            ),
                          ),
                        );
                      });
                    },
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

}

showPopup({
  required BuildContext context,
  required WidgetBuilder builder,
  double? elevation,
  Color? shadowColor,
  Duration animationDuration = const Duration(milliseconds: 200)
}) {
  final RenderBox button = context.findRenderObject()! as RenderBox;
  final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;

  Offset offset = Offset(0.0, button.size.height);

  RelativeRect position = RelativeRect.fromRect(
    Rect.fromPoints(
      button.localToGlobal(offset, ancestor: overlay),
      button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay),
    ),
    Offset.zero & overlay.size,
  );

  Navigator.of(context).push(_CustomPopupRoute(
      position: position,
      builder: builder,
      elevation: elevation,
      shadowColor: shadowColor,
      animationDuration: animationDuration,
      barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel
  ));
}

class _CustomPopupRoute<T> extends PopupRoute<T> {

  final WidgetBuilder builder;
  final RelativeRect position;
  final double? elevation;
  final Color? shadowColor;
  
  final String? barrierLabel;
  final Duration animationDuration;

  _CustomPopupRoute({
    required this.builder,
    required this.position,
    required this.barrierLabel,
    this.elevation,
    this.shadowColor,
    Duration? animationDuration
  }): animationDuration = animationDuration ?? const Duration(milliseconds: 200),
  super(traversalEdgeBehavior: TraversalEdgeBehavior.closedLoop);

  
  Color? get barrierColor => null;

  
  bool get barrierDismissible => true;

  
  Duration get transitionDuration => animationDuration;

  
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
    EdgeInsets padding = MediaQuery.paddingOf(context);
    final CurveTween heightFactorTween = CurveTween(curve: const Interval(0.0, 1.0));
    return MediaQuery.removePadding(
      context: context,
      removeTop: true,
      removeBottom: true,
      removeLeft: true,
      removeRight: true,
      child: CustomSingleChildLayout(
        delegate: _CustomPopupRouteLayout(position, padding),
        child: AnimatedBuilder(
          animation: animation,
          builder: (context, child) {
            return Material(
              child: _HeightFactorBox(
                heightFactor: heightFactorTween.evaluate(animation),
                child: child,
              )
            );
          },
          child: builder(context),
        ),
      ),
    );
  }

}

class _CustomPopupRouteLayout extends SingleChildLayoutDelegate {

  final RelativeRect position;
  EdgeInsets padding;
  double childHeightMax = 0;

  _CustomPopupRouteLayout(this.position, this.padding);

  
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    Size buttonSize = position.toSize(constraints.biggest);

    double constraintsWidth = buttonSize.width;
    double constraintsHeight = max(position.top - buttonSize.height - padding.top - kToolbarHeight, constraints.biggest.height - position.top - padding.bottom);

    return BoxConstraints.loose(Size(constraintsWidth, constraintsHeight));
  }

  
  Offset getPositionForChild(Size size, Size childSize) {
    double x = position.left;
    double y = position.top;
    final double buttonHeight = size.height - position.top - position.bottom;
    double constraintsHeight = max(position.top - buttonHeight - padding.top - kToolbarHeight, size.height - position.top - padding.bottom);
    if(position.top + constraintsHeight > size.height - padding.bottom) {
      y = position.top - childSize.height - buttonHeight;
    }

    return Offset(x, y);
  }

  
  bool shouldRelayout(covariant _CustomPopupRouteLayout oldDelegate) {
    return position != oldDelegate.position || padding != oldDelegate.padding;
  }
}

class _RenderHeightFactorBox extends RenderShiftedBox {
  double _heightFactor;
  _RenderHeightFactorBox({
    RenderBox? child,
    double? heightFactor,
  }):_heightFactor = heightFactor ?? 1.0, super(child);

  double get heightFactor => _heightFactor;

  set heightFactor(double value) {
    if (_heightFactor == value) {
      return;
    }
    _heightFactor = value;
    markNeedsLayout();
  }

  
  void performLayout() {
    final BoxConstraints constraints = this.constraints;

    if (child == null) {
      size = constraints.constrain(Size.zero);
      return;
    }

    child!.layout(constraints, parentUsesSize: true);

    size = constraints.constrain(Size(
      child!.size.width,
      child!.size.height,
    ));

    child!.layout(constraints.copyWith(maxWidth: size.width, maxHeight: size.height * heightFactor), parentUsesSize: true);

    size = constraints.constrain(Size(
      child!.size.width,
      child!.size.height,
    ));
  }
}

class _HeightFactorBox extends SingleChildRenderObjectWidget {

  final double? heightFactor;

  const _HeightFactorBox({
    super.key,
    this.heightFactor,
    super.child,
  });

  
  RenderObject createRenderObject(BuildContext context) => _RenderHeightFactorBox(heightFactor: heightFactor);

  
  void updateRenderObject(BuildContext context, _RenderHeightFactorBox renderObject) {
    renderObject.heightFactor = heightFactor ?? 1.0;
  }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1139745.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MedNeXt: Transformer-driven Scaling ofConvNets for Medical Image Segmentation

论文标题;MedNeXt: Transformer-driven Scaling of ConvNets for Medical Image Segmentation 论文链接&#xff1a;2303.09975.pdf (arxiv.org)https://arxiv.org/pdf/2303.09975.pdf 论文&#xff1a; MedNeXt&#xff1a;用于医学图像分割的转换器驱动的ConvNets缩放 项目…

echart绘制环形进度条

原型: <template><div class="chart"><div ref="chartRef" class="chart-bar" :style="{ width, height }"></div><div class="num">{{ sideText }}</div></div> </templa…

一文教你如何将Eclipse项目导入到IDEA运行

&#x1f4d6;本篇超级详细案例截图教学 Eclipse web项目如何导入到Intellij IDEA中&#xff0c;图片点击可放大仔细看 工具版本说明&#xff1a; 工具 版本 Intellij IDEA 2022.3 tomcat 8.5 JDK 1.8 步骤一 .导入一个已存在的工程 1.1 File–>New–>Proj…

损失函数总结(八):MultiMarginLoss、MultiLabelMarginLoss

损失函数总结&#xff08;八&#xff09;&#xff1a;MultiMarginLoss、MultiLabelMarginLoss 1 引言2 损失函数2.1 MultiMarginLoss2.2 MultiLabelMarginLoss 3 总结 1 引言 在前面的文章中已经介绍了介绍了一系列损失函数 (L1Loss、MSELoss、BCELoss、CrossEntropyLoss、NLL…

LIS系统-实现检验报告集中管理

LIS系统即实验室信息管理系统。LIS系统能实现临床检验信息化&#xff0c;检验科信息管理自动化。其主要功能是将检验科的实验仪器传出的检验数据经数据分析后&#xff0c;自动生成打印报告&#xff0c;通过网络存储在数据库中&#xff0c;使医生能够通过医生工作站方便、及时地…

基于springboot实现网吧管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现网吧管理系统演示 摘要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#x…

JDK11下载、安装与配置、运行第一个Java程序教程

JDK已经更新到20.0.2了&#xff0c;JDK11是相对比较稳定的版本&#xff0c;网上的JDK11安装配置教程一大堆&#xff0c;但是很多都过时了&#xff0c;自己整理了一篇JDK11下载安装的详细步骤&#xff0c;带有每一步的完整的图文教程&#xff0c;大家可以根据自己的需要下载。 …

pytorch 入门 (五)案例三:乳腺癌识别识别-VGG16实现

本文为&#x1f517;小白入门Pytorch内部限免文章 &#x1f368; 本文为&#x1f517;小白入门Pytorch中的学习记录博客&#x1f366; 参考文章&#xff1a;【小白入门Pytorch】乳腺癌识别&#x1f356; 原作者&#xff1a;K同学啊 在本案例中&#xff0c;我将带大家探索一下深…

pytorch深度学习实践(二):梯度下降算法详解和代码实现(梯度下降、随机梯度下降、小批量梯度下降的对比)

目录 一、梯度下降1.1 公式与原理1.1.1 cost(w)1.1.2 梯度1.1.3 w的更新 1.2 训练过程可视化1.3 代码实现 二、随机梯度下降&#xff08;stochastic gradient descent&#xff0c;SDG&#xff09;2.1 公式与原理2.1.1 w的更新 2.2 代码实现2.3 梯度下降和随机梯度下降的优缺点对…

漏洞复现-jquery-picture-cut 任意文件上传_(CVE-2018-9208)

jquery-picture-cut 任意文件上传_&#xff08;CVE-2018-9208&#xff09; 漏洞信息 jQuery Picture Cut v1.1以下版本中存在安全漏洞CVE-2018-9208文件上传漏洞 描述 ​ picture cut是一个jquery插件&#xff0c;以友好和简单的方式处理图像&#xff0c;具有基于bootstrap…

Vue3-小兔鲜项目

1.初始化项目 npm init vuelatest src目录调整 Git项目管理 基于create-vue创建出来的项目默认没有初始化git仓库&#xff0c;需要我们手动初始化 执行命令并完成首次提交 1.git init 2.git add 3.git commit -m "init" 别名路径联想提示 什么是别名路径联想…

通过requests库使用HTTP编写的爬虫程序

使用Python的requests库可以方便地编写HTTP爬虫程序。以下是一个使用requests库的示例&#xff1a; import requests# 发送HTTP GET请求 response requests.get("http://example.com")# 检查响应状态码 if response.status_code 200:# 获取响应内容html response.…

推荐5款助你高效工作的小软件

现在&#xff0c;有很多实用的工具和软件可以帮助我们更高效地完成各种任务。以下是5款值得推荐的工具软件&#xff0c;能够极大地提高我们的工作效率。 1.电子书阅读器——Koodo Reader ​ Koodo Reader 是一款开源免费的电子书阅读器&#xff0c;支持多达15种主流电子书格式…

laravel+vue2 element 一套项目级医院手术麻醉信息系统源码

手术麻醉临床信息系统源码&#xff0c;PHPmysqllaravelvue2 手术麻醉临床信息系统&#xff0c;采用计算机和通信技术&#xff0c;实现监护仪、麻醉机、输液泵等设备输出数据的自动采集&#xff0c;采集的数据能够如实准确地反映患者生命体征参数的变化&#xff0c;并实现信息高…

搜维尔科技:【应用】配备MTi-3的轻便型ROV,在水下进行地理标记视觉检测

部署潜水员进行水下摄像&#xff0c;不仅难度高而且费用昂贵&#xff0c;需要受过潜水和摄像两方面培训的专业人员来进行。但有些水下作业任务例如拍摄海底管道内部的照片&#xff0c;由于人员无法进入或危险度高的原因&#xff0c;无法由潜水员完成。 如今&#xff0c;俄罗…

看谷歌浏览器源码,为什么p标签和div标签为块元素

看谷歌浏览器源码 谷歌源码路径&#xff1a;third_party/blink/renderer/core/html/resources/html.css 为什么块级元素独占一行&#xff1f; 是谷歌浏览器设置div的默认样式 display:block 它才独占一行 p标签和div标签为块元素 strong,b,i,em等等标签为行内元素

如何在Excel中实现三联类模板?

本文由葡萄城技术团队原创并首发。转载请注明出处&#xff1a;葡萄城官网&#xff0c;葡萄城为开发者提供专业的开发工具、解决方案和服务&#xff0c;赋能开发者。 前言 在一些报表打印应用场景中&#xff0c;会有类似于如下图所示的排版格式&#xff1a; 一般情况下将这种类…

k8s statefulSet 学习笔记

缩写: sts 通过 kubectl api-resources 可以查到&#xff1a; NAMESHORTNAMESAPIVERSIONNAMESPACEDKINDstatefulsetsstsapps/v1trueStatefulSet web-sts.yaml apiVersion: v1 kind: Service metadata:name: nginxlabels:app: nginx spec:ports:- port: 80name: web-sts-svc…

22年上半年下午题

第一大题题目 第一大题解答 第一小问 看加工交互和说明来得出实体的名字。如果不太确定&#xff0c;可以多去看几条数据流来确认答案。仔细一点&#xff0c;这分稳啦。 第二小问 需要对应加工结合说明得出数据存储的名称。 一般可以在后面加上表字或者加上信息表。自拟&…

2023年Q3企业邮箱安全性报告:境内钓鱼邮件超过境外攻击

10月25日&#xff0c;Coremail邮件安全联合北京中睿天下信息技术有限公司发布《2023年第三季度企业邮箱安全性研究报告》。2023年第三季度企业邮箱安全呈现出何种态势&#xff1f;作为邮箱管理员&#xff0c;我们又该如何做好防护&#xff1f; 以下为精华版阅读&#xff0c;如需…