<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Zane&apos;s website</title><description>郑闯的个人网站</description><link>https://zheng-chuang.synology.me/</link><item><title>Flutter 状态管理</title><link>https://zheng-chuang.synology.me/posts/flutter-status-manage/</link><guid isPermaLink="true">https://zheng-chuang.synology.me/posts/flutter-status-manage/</guid><pubDate>Sun, 15 Sep 2024 09:04:54 GMT</pubDate><content:encoded>
## 状态提升

状态提升是 Flutter 中的一种状态管理方法，用于在多个组件之间共享状态。通过将状态提升到组件树的更高层次，可以更容易地管理和更新状态。

### 示例

假设我们有两个子组件，分别是 `Counter` 和 `CounterDisplay`，它们都用于显示计数器的值。我们可以将计数器的状态提升到父组件 `CounterApp` 中，并通过这两个子组件来展示状态。

```dart
class CounterApp extends StatefulWidget {
  @override
  _CounterAppState createState() =&gt; _CounterAppState();
}

class _CounterAppState extends State&lt;CounterApp&gt; {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text(&apos;Counter App&apos;)),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Counter(counter: _counter),
            CounterDisplay(counter: _counter),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: _incrementCounter,
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

class Counter extends StatelessWidget {
  final int counter;

  Counter({required this.counter});

  @override
  Widget build(BuildContext context) {
    return Text(&apos;Counter: $counter&apos;, style: TextStyle(fontSize: 24));
  }
}

class CounterDisplay extends StatelessWidget {
  final int counter;

  CounterDisplay({required this.counter});

  @override
  Widget build(BuildContext context) {
    return Text(&apos;The current count is: $counter&apos;, style: TextStyle(fontSize: 18));
  }
}
```


## 如何访问和修改外部状态

### 子组件访问父组件的状态

子组件访问父组件的状态很简单，只需要将父组件的状态作为参数传递给子组件即可。

```dart
class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() =&gt; _ParentWidgetState();
}

class _ParentWidgetState extends State&lt;ParentWidget&gt; {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ChildWidget(counter: _counter),
      ],
    );
  }
}

class ChildWidget extends StatelessWidget {
  final int counter;

  ChildWidget({required this.counter});

  @override
  Widget build(BuildContext context) {
    return Text(&apos;Counter: $counter&apos;);
  }
}
```

### 子组件修改父组件的状态

子组件修改父组件状态也很简单，通过传递一个回调函数给子组件，子组件在需要修改状态时调用该回调函数即可。

```dart
class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() =&gt; _ParentWidgetState();
}

class _ParentWidgetState extends State&lt;ParentWidget&gt; {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ChildWidget(onIncrement: _incrementCounter),
      ],
    );
  }
}

class ChildWidget extends StatelessWidget {
  final Function onIncrement;

  ChildWidget({required this.onIncrement});

  @override
  Widget build(BuildContext context) {
    return TextButton(onPressed: onIncrement, child: Text(&apos;Increment&apos;));
  }
}
```

## 所谓「控制器」到底是什么

### 示例

当我们使用 `TextField` 时，我们通常会使用 `TextEditingController` 来管理文本框的状态。我们可以通过 `TextEditingController` 的 `text` 属性来获取和设置文本框的文本内容。并且监听 `TextEditingController` 的 `text` 属性变化。



```dart
TextEditingController _controller = TextEditingController();

_controller.text = &apos;Hello, World!&apos;;

_controller.addListener(() {
  print(_controller.text);
});


TextField(
  controller: _controller,
)
```

### 自定义一个Controller

下面我们通过自己定义一个Controller来实现一个计数器。从而理解所谓的「控制器」到底是什么。

```dart
class CounterController extends ChangeNotifier {
  int _counter = 0;

  int get counter =&gt; _counter;

  set counter(int value) {
    _counter = value;
    notifyListeners();
  }

  void increment() {
    _counter++;
    notifyListeners();
  }
}


class CounterWidget extends StatelessWidget {


  final CounterController controller;

  CounterWidget({required this.controller});

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: controller,
      builder: (context, child) {
        return Text(&apos;Counter: ${controller.counter}&apos;);
      },
    );
  }
}

main() {
  runApp(
    MyApp(),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() =&gt; _HomePageState();
}

class _HomePageState extends State&lt;HomePage&gt; {

  final CounterController controller = CounterController();


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(&apos;Counter App&apos;)),
      body: Column(
        children: [
          CounterWidget(controller: controller),
          TextButton(onPressed: () {
            controller.increment();
          }, child: Text(&apos;Increment&apos;)),
        ],
      ),
    );
  }
}
```

&lt;iframe src=&quot;https://dartpad.cn/?id=318db1739821b6dc3ddc3ddad7bce6f3&quot; width=&quot;100%&quot; height=&quot;800px&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot;&gt; &lt;/iframe&gt;


### 详解 ChangeNotifier 机制

从上面可以看到，`ChangeNotifier` 机制的核心在于 `notifyListeners` 方法。每当状态发生变化时，我们都需要调用 `notifyListeners` 方法来通知所有监听该状态的组件。

至于组件如何监听状态的变化，我们使用了 `ListenableBuilder` 组件。当状态发生变化时，`ListenableBuilder` 组件会重新构建。从而实现状态的变化。

除了 `ChangeNotifier` 之外，还有 `ValueNotifer` 等组件，用于管理状态。


## 继承式组件 `InheritedWidget` 

`InheritedWidget` 是 Flutter 中的一种组件，用于在组件树中共享状态。通过继承 `InheritedWidget` 组件，我们可以将状态传递给子组件。

### 示例

```dart
import &apos;package:flutter/material.dart&apos;;

class MyColor extends InheritedWidget {
  final Color color;
  final Function() changeColor;

  MyColor({
    required this.color,
    required this.changeColor,
    required Widget child,
  }) : super(child: child);

  @override
  bool updateShouldNotify(MyColor oldWidget) {
    return oldWidget.color != color;
  }

  static MyColor of(BuildContext context) {
    // 根据类型获取最近的 MyColor 组件 并 添加依赖
    // 对应的还有 `context.getInheritedWidgetOfExactType&lt;MyColor&gt;()` 获取最近的 MyColor 组件 但不添加依赖 
    return context.dependOnInheritedWidgetOfExactType&lt;MyColor&gt;()!;
  }
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() =&gt; _MyAppState();
}

class _MyAppState extends State&lt;MyApp&gt; {
  Color _color = Colors.blue;

  void _changeColor() {
    setState(() {
      _color = _color == Colors.blue ? Colors.red : Colors.blue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyColor(
        color: _color,
        changeColor: _changeColor,
        child: Scaffold(
          appBar: AppBar(title: Text(&apos;MyColor Demo&apos;)),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const ColorDisplayWidget(),
                SizedBox(height: 20),
                ElevatedButton(
                  onPressed: _changeColor,
                  child: Text(&apos;Change Color&apos;),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class ColorDisplayWidget extends StatelessWidget {

  const ColorDisplayWidget({super.key});

  @override
  Widget build(BuildContext context) {
    final myColor = MyColor.of(context);
    return Container(
      width: 100,
      height: 100,
      color: myColor.color,
      child: Center(
        child: Text(
          &apos;Current Color&apos;,
          style: TextStyle(color: Colors.white),
        ),
      ),
    );
  }
}

void main() {
  runApp(MyApp());
}
```



&lt;iframe src=&quot;https://dartpad.cn/?id=d785e584aab374c3cafa6894ccc15ec6&quot; width=&quot;100%&quot; height=&quot;800px&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot;&gt; &lt;/iframe&gt;


上面的例子中 `ColorDisplayWidget` 是一个 `const` 组件，但是当我们点击按钮之后，`ColorDisplayWidget` 的颜色仍然能够发生变化。就是因为我们通过 `InheritedWidget` 的方式获取了`color`的值



&gt; 当 `dependOnInheritedWidgetOfExactType` 方法被调用时，Flutter 会自动将当前的 `BuildContext` 添加到 `InheritedWidget` 的依赖列表中。当 `InheritedWidget` 的状态发生变化时，Flutter 会自动调用 `updateShouldNotify` 方法，从而通知所有依赖于该状态的组件重新构建。 这时 子组件的 生命周期方法 `didChangeDependencies` 会被调用。


另外我们可以通过 `updateShouldNotifyDependents` 方法来决定是否通知所有依赖于该状态的组件重新构建。从而实现更精细化的状态管理。

```dart

class MyColor extends InheritedWidget {
  final Color color;
  final Function() changeColor;

  MyColor({
    required this.color,
    required this.changeColor,
    required Widget child,
  }) : super(child: child);

  @override
  bool updateShouldNotify(MyColor oldWidget) {
    return oldWidget.color != color;
  }


  @override
  bool updateShouldNotifyDependents(MyColor oldWidget, Set&lt;Object&gt; dependencies) {
    // 如果依赖列表发生变化，则通知所有依赖于该状态的组件重新构建
    if (dependencies.contains(&apos;color&apos;)) {
      return oldWidget.color != color;
    }
    return false;
  }

  static MyColor of(BuildContext context) {
    // 根据类型获取最近的 MyColor 组件 并 添加依赖
    // 对应的还有 `context.getInheritedWidgetOfExactType&lt;MyColor&gt;()` 获取最近的 MyColor 组件 但不添加依赖 
    return context.dependOnInheritedWidgetOfExactType&lt;MyColor&gt;()!;
  }
}

```


## 自己动手做个 Provider

我们的项目目录结构如下：

```
lib
├── change_notifier_provider.dart
├── main.dart
├── model
│   └── logo_model.dart
└── ui
    ├── control_panel.dart
    └── logo.dart
```

```dart
// change_notifier_provider.dart
import &apos;package:flutter/material.dart&apos;;

class ChangeNotifierProvider&lt;T extends Listenable&gt; extends StatefulWidget {
  const ChangeNotifierProvider(
      {super.key, required this.create, required this.child});

  final Widget child;
  final T Function(BuildContext context) create;

  @override
  State&lt;ChangeNotifierProvider&gt; createState() =&gt;
      _ChangeNotifierProviderState&lt;T&gt;();



  static T of&lt;T&gt;(BuildContext context, {required bool listen}) {
    if (listen) {
      return context.dependOnInheritedWidgetOfExactType&lt;_InnerInheritedWidget&lt;T&gt;&gt;()!.model;

    } else {
      return context.getInheritedWidgetOfExactType&lt;_InnerInheritedWidget&lt;T&gt;&gt;()!.model;

    }
  }

}

class _ChangeNotifierProviderState&lt;T extends Listenable&gt;
    extends State&lt;ChangeNotifierProvider&lt;T&gt;&gt; {
  late T model;

  @override
  void initState() {
    super.initState();
    model = widget.create(context);
  }

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: model,
      builder: (BuildContext context, Widget? child) {
        return _InnerInheritedWidget(
          model: model,
          child: child!,
        );
      },
      child: widget.child,
    );
  }
}

class _InnerInheritedWidget&lt;T&gt; extends InheritedWidget {
  const _InnerInheritedWidget({super.key, required super.child, required this.model});

  final T model;

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    return true;
  }
}

extension ChangeNotifierProviderExtension on BuildContext {
  T watch&lt;T extends Listenable&gt;() {
    return ChangeNotifierProvider.of&lt;T&gt;(this, listen: true);
  }

  T read&lt;T extends Listenable&gt;() {
    return ChangeNotifierProvider.of&lt;T&gt;(this, listen: true);
  }
}
```

```dart
// model/logo_model.dart
import &apos;package:flutter/material.dart&apos;;

class LogoModel extends ChangeNotifier {
  bool _flipX = false;

  bool _flipY = false;

  double _size = 50;

  bool get flipX =&gt; _flipX;

  set flipX(bool value) {
    _flipX = value;
    notifyListeners();
  }

  bool get flipY =&gt; _flipY;

  set flipY(bool value) {
    _flipY = value;
    notifyListeners();
  }

  double get size =&gt; _size;

  set size(double value) {
    _size = value;
    notifyListeners();
  }
}
```

```dart
// ui/control_panel.dart
import &apos;package:flutter/material.dart&apos;;
import &apos;/change_notifier_provider.dart&apos;;
import &apos;/model/logo_model.dart&apos;;

class ControlPanel extends StatefulWidget {
  const ControlPanel({super.key});

  @override
  State&lt;ControlPanel&gt; createState() =&gt; _ControlPanelState();
}

class _ControlPanelState extends State&lt;ControlPanel&gt; {
  @override
  Widget build(BuildContext context) {
    var model = context.watch&lt;LogoModel&gt;();
    return Card(
      margin: const EdgeInsets.all(32),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text(&apos;Size&apos;),
                Slider(
                  value: model.size,
                  min: 0,
                  max: 100,
                  onChanged: (value) {
                    model.size = value;
                  },
                ),
              ],
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text(&apos;FlipX&apos;),
                Switch(
                  value: model.flipX,
                  onChanged: (value) {
                    model.flipX = value;
                  },
                ),
                const SizedBox(width: 32),
                const Text(&apos;FlipY&apos;),
                Switch(
                  value: model.flipY,
                  onChanged: (value) {
                    model.flipY = value;
                  },
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
```
```dart
// ui/logo.dart
import &apos;package:flutter/material.dart&apos;;
import &apos;/change_notifier_provider.dart&apos;;
import &apos;/model/logo_model.dart&apos;;

class Logo extends StatelessWidget {
  const Logo({super.key});

  @override
  Widget build(BuildContext context) {
    var model = context.read&lt;LogoModel&gt;();
    return Card(
      child: Transform.flip(
        flipX: model.flipX,
        flipY: model.flipY,
        child: FlutterLogo(size: model.size + 50.0),
      ),
    );
  }
}

``` 

```dart
// main.dart
import &apos;package:flutter/material.dart&apos;;
import &apos;change_notifier_provider.dart&apos;;
import &apos;model/logo_model.dart&apos;;
import &apos;ui/control_panel.dart&apos;;
import &apos;ui/logo.dart&apos;;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: Colors.blue,
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State&lt;HomePage&gt; createState() =&gt; _HomePageState();
}

class _HomePageState extends State&lt;HomePage&gt; {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text(&apos;Home Page&apos;)),
      body: Center(
        child: ChangeNotifierProvider&lt;LogoModel&gt;(
          create: (BuildContext context) =&gt; LogoModel(),
          child: const Column(
            children: [
              Logo(),
              ControlPanel(),
            ],
          ),
        ),
      ),
    );
  }
}
```


 
### 效果

![image](../attachments/flutter-status-manage.gif)


## 参考资料

1. [状态提升\(Lifting-state-up\)](https://www.bilibili.com/video/BV1YN411S7bb)
2. [如何访问和修改外部状态](https://www.bilibili.com/video/BV1gh411P7c4)
3. [所谓「控制器」到底是什么](https://www.bilibili.com/video/BV1pW4y1o74U)
4. [详解ChangeNotifier机制](https://www.bilibili.com/video/BV1kM4y1x7qe)
5. [继承式组件InheritedWidget](https://www.bilibili.com/video/BV19V4y1b7UB)
6. [自己动手做个 Provider](https://www.bilibili.com/video/BV1k14y1z79j)
7. [利用泛型做通用封装](https://www.bilibili.com/video/BV11p4y157ar)
</content:encoded></item><item><title>Conda 和 Pip 设置国内镜像源教程</title><link>https://zheng-chuang.synology.me/posts/python-registry-set/</link><guid isPermaLink="true">https://zheng-chuang.synology.me/posts/python-registry-set/</guid><pubDate>Wed, 11 Sep 2024 09:55:58 GMT</pubDate><content:encoded>
# Conda 和 Pip 设置国内镜像源教程

在中国大陆使用 Conda 和 Pip 安装 Python 包时，由于网络原因可能会遇到下载速度慢或者连接超时的问题。为了解决这个问题，我们可以将默认的软件源更换为国内的镜像源。本教程将介绍如何为 Conda 和 Pip 设置国内镜像源。

## Conda 设置国内镜像源

Conda 是一个开源的包管理系统和环境管理系统。以下是设置 Conda 国内镜像源的步骤：

1. 打开终端（Terminal）。

2. 输入以下命令来添加清华大学的 Conda 镜像：

   ```bash
   conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
   conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
   conda config --set show_channel_urls yes
   ```

3. 设置搜索时显示通道地址：

   ```bash
   conda config --set show_channel_urls yes
   ```

4. 验证设置是否生效：

   ```bash
   conda config --show channels
   ```

   如果看到清华大学的镜像地址，说明设置成功。

## Pip 设置国内镜像源

Pip 是 Python 的包管理工具。以下是设置 Pip 国内镜像源的步骤：

1. 临时使用：

   在使用 pip 的时候，可以通过 `-i` 参数指定镜像源。例如：

   ```bash
   pip install some-package -i https://pypi.tuna.tsinghua.edu.cn/simple
   ```

2. 永久修改：

   - Linux 和 macOS 用户：
     1. 在用户目录下创建 pip 配置文件：
        ```bash
        mkdir ~/.pip
        ```
     2. 编辑 `~/.pip/pip.conf` 文件：
        ```bash
        nano ~/.pip/pip.conf
        ```
     3. 在文件中添加以下内容：
        ```
        [global]
        index-url = https://pypi.tuna.tsinghua.edu.cn/simple
        ```

   - Windows 用户：
     1. 在用户目录下创建 pip 文件夹，通常在 `C:\Users\Your_Username\pip`。
     2. 在 pip 文件夹中创建 `pip.ini` 文件。
     3. 在 `pip.ini` 文件中添加以下内容：
        ```
        [global]
        index-url = https://pypi.tuna.tsinghua.edu.cn/simple
        ```

3. 验证设置：

   运行以下命令：

   ```bash
   pip config list
   ```

   如果看到清华大学的镜像地址，说明设置成功。

通过以上步骤，你就可以使用国内镜像源来加速 Conda 和 Pip 的包下载速度了。记住，使用镜像源可能会导致获取最新包版本有slight延迟，如果你需要最新版本的包，可以临时切换回官方源。



</content:encoded></item><item><title>在 MacBook 上重置 PyCharm 试用时间</title><link>https://zheng-chuang.synology.me/posts/pycharm-reset-trial/</link><guid isPermaLink="true">https://zheng-chuang.synology.me/posts/pycharm-reset-trial/</guid><pubDate>Wed, 11 Sep 2024 09:29:41 GMT</pubDate><content:encoded>
# 在 MacBook 上重置 PyCharm 试用时间

PyCharm 是一款强大的 Python 集成开发环境（IDE），但其专业版需要付费使用。如果你想在购买之前充分体验 PyCharm 的所有功能，可以通过以下步骤重置试用期：

1. 关闭 PyCharm 应用程序。

2. 打开终端（Terminal）。

3. 输入以下命令来删除 PyCharm 的评估密钥文件：

   ```bash
   rm ~/Library/Application\ Support/JetBrains/PyCharm*/eval/*.key
   ```

4. 删除 PyCharm 的其他配置文件：

   ```bash
   rm -rf ~/Library/Preferences/JetBrains/PyCharm*
   rm -rf ~/Library/Caches/JetBrains/PyCharm*
   rm -rf ~/Library/Application\ Support/JetBrains/PyCharm*
   ```

5. 重新启动 PyCharm。

6. 当 PyCharm 启动时，它会要求你重新激活试用版。

请注意，这种方法仅用于评估目的。如果你发现 PyCharm 对你的工作流程有帮助，建议购买正版许可证以支持开发者并获得完整的功能和支持。

另外，JetBrains 还提供了针对学生、教师和开源项目维护者的免费许可证，如果你符合条件，可以申请这些计划。

最后，请记住经常性地重置试用期可能违反 PyCharm 的使用条款。长期使用建议购买正版许可或考虑使用 PyCharm 社区版，它是免费的并提供了许多核心功能。
</content:encoded></item><item><title>约束、尺寸、位置</title><link>https://zheng-chuang.synology.me/posts/tutorials/flutter/layout/1/</link><guid isPermaLink="true">https://zheng-chuang.synology.me/posts/tutorials/flutter/layout/1/</guid><pubDate>Wed, 11 Sep 2024 08:55:37 GMT</pubDate><content:encoded>
## 布局原则

**向下传递约束、向上传递尺寸、最后父级得到尺寸之后决定每个元素放在哪里**

例如下面的例子，`[root]`向下传递一个紧约束(尺寸位屏幕的宽度和高度),所以`Container`设置的`width`和`height`没有生效，`Center`组件向`FlutterLogo`设置了位置信息，所以`FlutterLogo`被居中展示

&lt;iframe src=&quot;https://dartpad.cn/?id=389ae518ef22330fdaeef30d7b22b428&quot; width=&quot;100%&quot; height=&quot;800px&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot;&gt; &lt;/iframe&gt;


## 约束概念

1. **紧约束**：尺寸的最小值和尺寸的最大值相等的约束是紧约束
2. **松约束**：尺寸的最小值为0的约束是松约束

如果有约束最大是0，最小也是0，那么这个约束同时是紧约束和松约束
</content:encoded></item><item><title>获取和设置布局约束</title><link>https://zheng-chuang.synology.me/posts/tutorials/flutter/layout/2/</link><guid isPermaLink="true">https://zheng-chuang.synology.me/posts/tutorials/flutter/layout/2/</guid><pubDate>Wed, 11 Sep 2024 08:55:37 GMT</pubDate><content:encoded>
## 获取布局约束

我们可以通过`LayoutBuilder`来获取父组件传递的布局的约束信息

```dart
import &apos;package:flutter/material.dart&apos;;

void main() {
  runApp(
    Container(
      color: Colors.red[200],
      child: LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
          print(&apos;constraints $constraints&apos;);
          return const Center(
            child: FlutterLogo(
              size: 50,
            ),
          );
        },
      ),
    ),
  );
}
```

## 设置布局约束

我们可以通过`ConstrainedBox`来设置向子组件传递的约束信息

```dart
import &apos;package:flutter/material.dart&apos;;

void main() {
  runApp(
    Container(
      color: Colors.red[200],
      child: ConstrainedBox(
        constraints: const BoxConstraints(
          minWidth: 0,
          maxWidth: 400,
          maxHeight: 0,
          minHeight: 400,
        ),
        child: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            print(&apos;constraints $constraints&apos;);
            return const Center(
              child: FlutterLogo(
                size: 50,
              ),
            );
          },
        ),
      ),
    ),
  );
}
```
&lt;iframe src=&quot;https://dartpad.cn/?id=690265eb5dffecc323cdb43cc08f3b77&quot; width=&quot;100%&quot; height=&quot;800px&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot;&gt; &lt;/iframe&gt;

</content:encoded></item><item><title>Python下载及安装</title><link>https://zheng-chuang.synology.me/posts/tutorials/python/1/</link><guid isPermaLink="true">https://zheng-chuang.synology.me/posts/tutorials/python/1/</guid><pubDate>Wed, 11 Sep 2024 08:55:37 GMT</pubDate><content:encoded>

## 在Mac上安装Python
如果你正在使用Mac，那么系统自带的Python版本是2.7。要安装最新的Python 3，有两个方法：

方法一：从Python官网下载Python 3 macOS版的[安装程序](https://www.python.org/downloads/macos/)，下载后双击运行并安装；

方法二：如果安装了Homebrew，直接通过命令`brew install python3`安装即可。

方法三：使用anaconda整合包，点击[下载地址](https://www.anaconda.com/download/success)即可下载安装
![img.png](img.png)


## 参考资料

### 视频
[和我一起玩Python——ep1. Python下载及安装](https://www.bilibili.com/video/BV1nG41117tg/?spm_id_from=333.788&amp;vd_source=65ae1b73a4f38e90efac59d31469d080)

### 文档
[Install Homebrew](https://brew.sh/zh-tw/)

[安装Python](https://liaoxuefeng.com/books/python/install/)</content:encoded></item><item><title>使用pandas处理excel</title><link>https://zheng-chuang.synology.me/posts/tools/pandas-excel/</link><guid isPermaLink="true">https://zheng-chuang.synology.me/posts/tools/pandas-excel/</guid><pubDate>Wed, 11 Sep 2024 08:55:37 GMT</pubDate><content:encoded>
## 读取excel
```python
import pandas as pd

# 文件路径
file_path = &quot;./data.xlsx&quot;

# sheet_name 工作表名 根据具体情况传递，可以为空
# header 从第几行读取，默认为 0
sheet = pd.read_excel(file_path, sheet_name=&quot;单词管理&quot;, header=1)

```
例如下面这张excel表，`sheet_name`就是`单词管理`，`header`就是第二行也就是应该传递`1`

![img.png](img.png)

## 获取列索引和行索引

```python
import pandas as pd

path = &quot;./data.xlsx&quot;

sheet = pd.read_excel(path, sheet_name=&quot;单词管理&quot;, header=1)

# 获取列索引
columns = sheet.columns
# 获取行索引
index = sheet.index
```
![img_1.png](img_1.png)
![img_2.png](img_2.png)


## 选择数据

### 选择单列数据

```python
import pandas as pd

path = &quot;./data.xlsx&quot;

sheet = pd.read_excel(path,  sheet_name=&quot;单词管理&quot;, header=1)

sheet[&apos;所属词包&apos;]

```

![img_3.png](img_3.png)


### 选择多列数据
```python
import pandas as pd

path = &quot;./data.xlsx&quot;

sheet = pd.read_excel(path,  sheet_name=&quot;单词管理&quot;, header=1)

sheet[[&apos;所属词包&apos;, &apos;英文单词&apos;]]
```
![img_4.png](img_4.png)

### 选择连续列或连续行的数据

```python
import pandas as pd

path = &quot;./data.xlsx&quot;

sheet = pd.read_excel(path,  sheet_name=&quot;单词管理&quot;, header=1)

# sheet.iloc[0:10,0:5] # 读取0到10行，0到5列的数据

# iloc 接受参数 [行切片,列切片]
sheet.iloc[0:,0:5] # 读取所有行，0到5列的数据
```
![img_5.png](img_5.png)

### 选择行数据
```python
import pandas as pd

path = &quot;./data.xlsx&quot;

sheet = pd.read_excel(path,  sheet_name=&quot;单词管理&quot;, header=1)

sheet.loc[[1]] # 选择单行数据
sheet.loc[[2,3,4]] # 选择多行数据
sheet.loc[range(0, 100)] # 传入一个切片选择数据

```

![img_6.png](img_6.png)


## 数据运算和排序
```python
import pandas as pd

path = &quot;./data.xlsx&quot;

sheet = pd.read_excel(path,  sheet_name=&quot;单词管理&quot;, header=1)

sheet[&apos;拼接数据&apos;] = sheet[&apos;英文单词&apos;] + sheet[&apos;中文释义&apos;]
```

![img_7.png](img_7.png)

```python
import pandas as pd

path = &quot;./data.xlsx&quot;

sheet = pd.read_excel(path,  sheet_name=&quot;单词管理&quot;, header=1)

sheet.sort_values(by=[&apos;英文单词&apos;], inplace=True) # 默认从小到大排序，inplace=True 表示直接修改原数据
sheet[: 20]
```

![img_8.png](img_8.png)


## 筛选数据
```python
import pandas as pd

path = &quot;./data.xlsx&quot;

sheet = pd.read_excel(path,  sheet_name=&quot;单词管理&quot;, header=1)
xiaoxue_sheet = sheet[sheet[&apos;所属年级&apos;] == &apos;小学&apos;]
chuzhong_sheet = sheet[sheet[&apos;所属年级&apos;] == &apos;初中&apos;]
gaozhong_sheet = sheet[sheet[&apos;所属年级&apos;] == &apos;高中&apos;]

```

## 导出到xlsx

```python
import pandas as pd

path = &quot;./data.xlsx&quot;

sheet = pd.read_excel(path,  sheet_name=&quot;单词管理&quot;, header=1)
xiaoxue_sheet = sheet[sheet[&apos;所属年级&apos;] == &apos;小学&apos;]
chuzhong_sheet = sheet[sheet[&apos;所属年级&apos;] == &apos;初中&apos;]
gaozhong_sheet = sheet[sheet[&apos;所属年级&apos;] == &apos;高中&apos;]

with pd.ExcelWriter(&apos;./data1.xlsx&apos;) as writer:
    xiaoxue_sheet.to_excel(writer, sheet_name=&quot;小学词汇&quot;)
    chuzhong_sheet.to_excel(writer, sheet_name=&quot;初中词汇&quot;)
    gaozhong_sheet.to_excel(writer, sheet_name=&quot;高中词汇&quot;)
```


![img_9.png](img_9.png)


## 数据去重

```python
import pandas as pd

path = &quot;./data.xlsx&quot;

sheet = pd.read_excel(path,  sheet_name=&quot;单词管理&quot;, header=1)


&apos;&apos;&apos;
keep: &apos;first&apos; 表示保留第一个重复项（默认值）
      &apos;last&apos; 表示保留最后一个重复项
      False 表示删除全部重复项
      
inplace: 是否更改原数据
ignore_index: 是否重新索引
&apos;&apos;&apos;
sheet.drop_duplicates(keep=&apos;first&apos;, inplace=True, ignore_index=False)


```</content:encoded></item><item><title>Unable to boot the simulator</title><link>https://zheng-chuang.synology.me/posts/tools/unable-to-boot-the-simulator/</link><guid isPermaLink="true">https://zheng-chuang.synology.me/posts/tools/unable-to-boot-the-simulator/</guid><pubDate>Wed, 11 Sep 2024 08:55:37 GMT</pubDate><content:encoded>
1. 打开系统设置
2. 搜索储存空间
3. 选择开发者
4. 删除 Xcode Caches

![Unable to boot the simulator](Unable-to-boot-the-simulator.png)</content:encoded></item><item><title>Let’s Encrypt SSL 泛域名证书申请和配置</title><link>https://zheng-chuang.synology.me/posts/tools/acme-usage/</link><guid isPermaLink="true">https://zheng-chuang.synology.me/posts/tools/acme-usage/</guid><pubDate>Wed, 11 Sep 2024 08:55:37 GMT</pubDate><content:encoded>
&gt; 本文转载自[Let’s Encrypt SSL 泛域名证书申请和配置](https://zhuanlan.zhihu.com/p/445852299?utm_id=0)

为了在网站上启用 HTTPS，我们需要从证书颁发机构（CA）申请 SSL 证书。Let’s Encrypt 是一个证书颁发机构，向 Let’s Encrypt 申请证书是免费的。Let’s Encrypt 支持泛域名证书，不需要为每个子域名单独申请证书。本文以申请泛域名证书为例，详细介绍安装和配置 SSL 证书的过程。

目前常用的 Let’s Encrypt 证书生成工具有 `certbot`、`acme.sh`、`acme-tiny`，本文使用的是 `acme.sh`。`acme.sh` 申请和安装泛域名 SSL 证书相对来说是比较方便的。


## 1. 安装 acme.sh

安装过程需要服务器已安装 `socat` 模块，它是一个多功能的网络小工具。

```shell
dnf install socat -y
```

通过下面命令安装 `acme.sh` ，Email 用来接收重要重要通知，如证书快到期未更新会收到通知。

```shell
curl https://get.acme.sh | sh -s email=my@example.com
```

执行命令后几秒就安装好了，如果半天没有反应请`Ctrl+C` 后重新执行命令。`acme.sh` 安装在 `~/.acme.sh` 目录下，并自动创建了一个 `cronjob`，每天 0:00 点自动检测所有的证书，如果快过期了, 则会自动更新。


安装后，理论上会自动添加一个 `acme.sh` 全局应用别名，但有时候会 `command not found`，需要手动执行以下命令：`source ~/.bashrc` 或 `source ~/.bash_profile`，或关掉终端重新打开，然后再继续下一步。

## 2. 生成证书

`acme.sh` 实现了 `acme` 协议支持的所有验证协议，一般有三种验证方式：HTTP 方式、手动 DNS 方式和 DNS API 方式。**推荐使用 DNS API 方式**。

### 2-1 HTTP 方式

HTTP 方式需要在你的网站根目录下放置一个文件, 来验证你的域名所有权以完成验证，然后就可以生成证书了。

```shell
acme.sh --issue -d example.com --webroot /path/to/example.com/
```

注意，HTTP 验证方式不支持生成泛域名证书。

### 2-2 手动 DNS 方式
这种方式需要手动在域名上添加一条 TXT 解析记录，验证域名所有权。这种方式的好处是, 你不需要任何服务器，不需要任何公网 IP，只需要 DNS 的解析记录即可完成验证。

执行以下命令进行手动 DNS 验证：

```shell
acme.sh --issue -d example.com -d &quot;*.example.com&quot; --dns \
--yes-I-know-dns-manual-mode-enough-go-ahead-please
```
到 DNS 解析中，新增一条 TXT 记录，域名前缀为 `_acme-challenge`，记录值为终端输出的 `TXT value` 的值。然后再次执行：


```shell
acme.sh --renew -d example.com -d &quot;*.example.com&quot; \
--yes-I-know-dns-manual-mode-enough-go-ahead-please
```

注意：这种方式的坏处是，如果不同时配置 DNS API，将无法自动更新证书，每次都需要手动再次重新解析验证域名所有权。


### 2-3 DNS API 方式


自动 DNS 验证方式需要使用域名解析服务商的 DNS API，像腾讯云（DNSPos）和阿里云都提供 DNS API 功能。

如果你用的是腾讯云 DNSPod，在右上角头像下拉中选择“API 密钥”，点击“DNSPos Token”创建密钥：

![DPDNSPos](DPDNSPos.webp)

如果你用的是阿里云，在右上角头像下拉中选择“AccessKey 管理”，创建密钥：

![AliDNSPos](AliDNSPos.webp)

创建好密钥后，使用如下命令把它们的 Id 和 Token 或 Secret 放到环境变量中：

```shell
# 腾讯云
export DP_Id=&quot;YourId&quot;
export DP_Key=&quot;YourToken&quot;

# 阿里云
export Ali_Key=&quot;YourAccessKeyId&quot;
export Ali_Secret=&quot;YourAccessKeySecret&quot;
```

再通过下面命令生成证书：

```shell
# 腾讯云
acme.sh --issue --dns dns_dp -d &quot;example.com&quot; -d &quot;*.example.com&quot;

# 阿里云
acme.sh --issue --dns dns_ali -d &quot;example.com&quot; -d &quot;*.example.com&quot;
```

注意：这里第一个域名为顶级域名，后面一个为泛域名。这种方式将自动为你的域名添加一条 TXT 解析，验证成功后，这条解析记录会被删除，对你来说是无感的。

证书生成成功后，默认保存在 `~/.acme.sh/example.com/` 目录中。请不要直接使用 `~/.acme.sh/` 目录下的文件，这里面的文件都是内部使用的，而且目录结构可能会变化，我们需要把证书复制到需要用的地方去。

## 3. 安装证书

在 `~/.acme.sh/example.com/` 目录生成的证书文件中，我们主要需要用到两个文件：`fullchain.cer` 和 `example.com.key`。下面以 Nginx 为例，来看看如何安装证书。

### 3-1 创建通用 SSL 配置文件
在 `/etc/nginx/` 目录下，创建一个为名 `ssl-options.conf` 的 SSL 通用配置文件，内容参考如下：

```conf
ssl_protocols               TLSv1.2 TLSv1.3;
ssl_ciphers                 ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers   off;
ssl_session_cache           shared:le_nginx_SSL:10m;
ssl_session_timeout         1200m;
ssl_session_tickets         on;
ssl_stapling                on;
```

参数说明：

- `ssl_protocols`：加密协议；
- `ssl_ciphers`：加密算法；
- `ssl_prefer_server_ciphers`：服务端加密算法优先；
- `ssl_session_cache`：会话缓存；
- `ssl_session_timeout`：用户会话缓存失效时间，对安全性有高要求的站点需要降低该值；
- `ssl_stapling`：启用 OCSP 可减少用户验证证书的时间；
- `ssl_session_tickets`：为复用会话创建或加载 Ticket Key。 

### 3-2 生成 dhparam.pem
OpenSSL 的 dhparam 用于生成和管理 dh 文件。dh(Diffie-Hellman) 是著名的密钥交换协议，它可以保证通信双方安全地交换密钥。

使用如下命令生成一个 `dhparam.pem` 文件：

```shell
openssl dhparam -out /etc/nginx/dhparam.pem 2048
```

### 3-3 为 Nginx 站点配置证书
先为网站的证书创建一个存放目录：

```shell
mkdir -p /etc/nginx/cert/example.com
```

目录中的文件我们后面通过脚本复制进来，这里先不管。

打开对应的 Nginx 站点配置文件，例如：`/etc/nginx/conf.d/example.com.conf`，参考编辑其内容如下：

```conf
# /etc/nginx/conf.d/example.com.conf
server {
    listen              80;
    server_name         example.com www.example.com;
    return              301 https://$host$request_uri;
}

server {
    listen              443 ssl;
    server_name         example.com www.example.com;
    server_tokens       off; # 禁止在响应报文中包含Nginx版本信息

    ssl_certificate     /etc/nginx/cert/example.com/fullchain.cer;
    ssl_certificate_key /etc/nginx/cert/example.com/example.com.key;
    include             /etc/nginx/ssl-options.conf;
    ssl_dhparam         /etc/nginx/dhparam.pem;

    if ($host != &apos;example.com&apos; ) {
        return          301 https://example.com$request_uri;
    }

    location / {
        proxy_pass          http://127.0.0.1:3000;
        proxy_http_version  1.1;
        proxy_set_header    Upgrade $http_upgrade;
        proxy_set_header    Connection keep-alive;
        proxy_set_header    Host $host;
        proxy_cache_bypass  $http_upgrade;
        proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header    X-Forwarded-Proto $scheme;
    }
}
```

### 3-4 安装和自动更新证书
不要直接复制证书到目标目录，正确的方式是使用 `-install-cert` 命令安装。在命令中指定目标位置，证书文件会被复制到相应的位置。并且，可以指定 `reloadcmd` 命令，当证书更新以后，`reloadcmd` 命令会被自动调用，让新的配置生效。

执行如下安装命令完成证书安装：
```shell
acme.sh --install-cert -d example.com \
--key-file       /etc/nginx/cert/example.com/example.com.key \
--fullchain-file /etc/nginx/cert/example.com/fullchain.cer \
--reloadcmd      &quot;systemctl force-reload nginx&quot;
```

这里指定的所有参数都会被自动记录下来，并在将来证书自动更新以后, 被再次自动调用。证书在到期之前会自动更新，你无需任何操作。

## 4. 更新 acme.sh
目前由于 `acme` 协议和 Let’s Encrypt CA 都在频繁的更新，因此 `acme.sh` 也经常更新以保持同步。

手动升级 `acme.sh` 到最新版：

```shell
acme.sh --upgrade
```

如果你不想手动升级，可以开启自动升级：

```shell
acme.sh --upgrade --auto-upgrade
```

你也可以随时关闭自动更新：

```shell
acme.sh --upgrade --auto-upgrade 0
```


## 5. 参考

[acme.sh 官方文档](https://github.com/acmesh-official/acme.sh/wiki)</content:encoded></item><item><title>群晖WebStation中使用Nginx作为服务器配置自定义规则-地址重写｜伪静态｜文件索引</title><link>https://zheng-chuang.synology.me/posts/synology/1/</link><guid isPermaLink="true">https://zheng-chuang.synology.me/posts/synology/1/</guid><pubDate>Wed, 11 Sep 2024 08:55:37 GMT</pubDate><content:encoded>
## 说明

我使用的设备情况如下：

- 硬件：DS920+
- 系统：DSM 7.1-42661 Update 1
- 套件版本：WebStation 3.1.0-0339

## 必要条件

- 开启DSM的SSH访问功能（通常在**控制面板-终端机和SNMP-启用SSH功能）
- 拥有管理员权限的账户（不一定是root，只要有管理员权限即可，这样可以使用sudo提权）

## 配置步骤

以搭建文件索引服务器为例，演示如何添加自定义配置。

## 使用WebStation新增静态网站

1. 选择`网页服务`并配置静态资源存放路径

![alt text](image.png)

2. 选择`网络门户`并配置域名地址

![alt text](image-1.png)

## 添加Nginx自定义配置

这一步需要在终端进行操作。使用SSH命令连接上DSM。

```shell
ssh user@nas.simaek.com
```

切换到sudo交互模式，避免每次输入命令都要添加sudo的麻烦。这里输入的密码是user用户的密码，user用户必须具有管理员群组权限（可在`控制面板-用户与群组-用户账户`中查看）。

```shell
sudo -i
Password:
```

切换到Nginx配置文件所在目录，目录下有4个子目录。以available结尾的目录中放置的文件并不一定会被启用。当在DSM系统中启用服务的时候，这些文件才会被链接到Nginx真正的配置目录并生效。并且这些配置文件都是以UUID的方式命名的。因此为了便于查看，我们直接操作链接文件即可。链接文件的名称更加具有可读性。

```shell
cd /usr/local/etc/nginx
ls -l
```

- `conf.d`：这里的文件都是链接到conf.d-available中的配置文件。
- `conf.d-available`：一些群晖套件的配置，用户自定义配置，通用选项配置等。
- `sites-available`：WebStation中的虚拟主机、套件服务器门户等配置。
- `sites-enabled`：这里的文件都是链接到sites-available中的配置文件。
由上可知，我们需要操作的配置文件是位于`/usr/local/etc/nginx/sites-enabled`目录中的`server.webstation-vhost.conf`，这个配置文件包含了所有WebStation中添加的虚拟主机。

使用vim编辑器打开此文件，文本如下：

```nginx
server {
    listen      80;
    listen      [::]:80;

    listen      443 ssl;
    listen      [::]:443 ssl;

    server_name dl.simaek.com ;

    if ( $host !~ &quot;(^dl.simaek.com$)&quot; ) { return 404; }

    include /usr/syno/etc/www/certificate/WebStation_vhost_d29555d8-e3ba-44ff-b82c-dfc1807fef13/cert.conf*;

    include /usr/syno/etc/security-profile/tls-profile/config/vhost_d29555d8-e3ba-44ff-b82c-dfc1807fef13.conf*;

    ssl_prefer_server_ciphers   on;

    location ^~ /.well-known/acme-challenge {
        root /var/lib/letsencrypt;
        default_type text/plain;
    }

    include conf.d/.webstation.error_page.default.conf*;

    include conf.d/.webstation.error_page.default.resource.conf*;
       
    root    &quot;/volume1/web/download&quot;;
    index    index.html  index.htm  index.cgi  index.php  index.php5 ;

    include /usr/local/etc/nginx/conf.d/d29555d8-e3ba-44ff-b82c-dfc1807fef13/user.conf*;

}
```

配置的关键就在于这一Server段配置的最后一行。引入user.conf为前缀的文件。默认情况下，此文件不会自动创建，但是此文件的父目录是存在的，我们需要自己创建这个文件。

```shell
cd /usr/local/etc/nginx/conf.d/d29555d8-e3ba-44ff-b82c-dfc1807fef13
touch user.conf
```

使用vim编辑器打开新创建的user.conf文件，填入以下内容。

```shell
location / {
    autoindex on;
    autoindex_exact_size off;
    autoindex_localtime on;

    if ($request_uri ~ ^/(.*)\.html(\?|$)) {
        return 302 /$1;
    }
    try_files $uri $uri.html $uri/ =404;

}
```

修改完成后，需要重新载入Nginx的配置生效，可以在WebStation中先停用虚拟主机再开启。另一种方式是使用Nginx的reload信号。

```shell
nginx -s reload
```

## 后续说明

自定义修改Nginx的配置，如果你不了解群晖的设计，也许会在`server.webstation-vhost.conf`上直接修改，这也是有效的。但是当你在WebStaion中进行配置变更的时候，所有的修改都会丢失。因为此配置文件是DSM动态生成的。而最后include包含的`user.conf`并不会因此受到影响。

Apache作为后端服务器也是类似的处理方式。 配置文件目录位于

`/usr/local/etc/apache{version}`，

`{version}`表示Apache的版本号，

例如2.4版为`/usr/local/etc/apache24`。</content:encoded></item><item><title>Flutter 字体描边效果</title><link>https://zheng-chuang.synology.me/posts/flutter/2/</link><guid isPermaLink="true">https://zheng-chuang.synology.me/posts/flutter/2/</guid><pubDate>Wed, 11 Sep 2024 08:55:37 GMT</pubDate><content:encoded>

```dart
import &apos;package:flutter/material.dart&apos;;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter 字体描边效果&apos;,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: &apos;Flutter 字体描边效果&apos;),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State&lt;MyHomePage&gt; createState() =&gt; _MyHomePageState();
}

class _MyHomePageState extends State&lt;MyHomePage&gt; {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Column(
        children: [
          HollowText(
            strokeColor: Colors.black,
            strokeWidth: 2,
            child: const Text(
              &apos;The quick brown fox jumps over the lazy dog.&apos;,
              style: TextStyle(
                fontSize: 40,
                color: Colors.red,
              ),
            ),
          ),
          HollowText(
            strokeColor: Colors.black,
            strokeWidth: 2,
            child: const Text(
              &apos;THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.&apos;,
              style: TextStyle(
                fontSize: 40,
                color: Colors.red,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class HollowText extends StatelessWidget {
  HollowText({
    super.key,
    required this.child,
    required this.strokeWidth,
    required this.strokeColor,
  }) : assert(child.style != null, &quot;style 不能为 null&quot;);

  final Text child;
  final double strokeWidth;
  final Color strokeColor;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Text(
          child.data!,
          style: child.style!.copyWith(
            foreground: Paint()
              ..style = PaintingStyle.stroke
              ..strokeCap = StrokeCap.round
              ..strokeJoin = StrokeJoin.round
              ..strokeWidth = strokeWidth
              ..color = strokeColor,
          ),
          maxLines: child.maxLines,
          overflow: child.overflow,
          semanticsLabel: child.semanticsLabel,
          softWrap: child.softWrap,
          strutStyle: child.strutStyle,
          textAlign: child.textAlign,
          textDirection: child.textDirection,
          textScaler: child.textScaler,
        ),
        child
      ],
    );
  }
}
```

&lt;iframe src=&quot;https://dartpad.cn/?id=0e7cc5c0ccfc747fb1c8084d51a52504&quot; width=&quot;100%&quot; height=&quot;800px&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot;&gt; &lt;/iframe&gt;

</content:encoded></item><item><title>Flutter：一个关于SliverFillRemaining的页面布局</title><link>https://zheng-chuang.synology.me/posts/flutter/1/</link><guid isPermaLink="true">https://zheng-chuang.synology.me/posts/flutter/1/</guid><pubDate>Wed, 11 Sep 2024 08:55:37 GMT</pubDate><content:encoded>
实现一个类似下面图片中的布局

![img.png](img.png)


```dart
import &apos;package:flutter/material.dart&apos;;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &apos;Flutter Demo&apos;,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: &apos;Flutter Demo Home Page&apos;),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State&lt;MyHomePage&gt; createState() =&gt; _MyHomePageState();
}

class _MyHomePageState extends State&lt;MyHomePage&gt; {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: SafeArea(
        child: SizedBox(
          width: double.infinity,
          child: CustomScrollView(
            slivers: [
              const SliverToBoxAdapter(
                child: Column(
                  children: [
                    FlutterLogo(size: 150),
                    FlutterLogo(size: 150),
                    FlutterLogo(size: 150),
                  ],
                ),
              ),
              SliverFillRemaining(
                hasScrollBody: false, // 禁止滚动
                child: Align(
                  alignment: Alignment.bottomCenter,
                  child: FilledButton(
                    onPressed: () {},
                    child: const Text(&quot;点击&quot;),
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}
```


&lt;iframe src=&quot;https://dartpad.cn/?id=ec9f6437852da91409adb74d5a1d8271&quot; width=&quot;100%&quot; height=&quot;800px&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot;&gt; &lt;/iframe&gt;


</content:encoded></item></channel></rss>