Rust+Slint 实现动态消息提示框源码分享

Rust+Slint 实现动态消息提示框源码分享

一、效果展示

二、源码分享

1、工程结构

2、main.rs

rust 复制代码
slint::include_modules!();

use slint::{Model, ModelRc, Timer, TimerMode, VecModel};
use std::cell::RefCell;
use std::rc::Rc;
use std::time::{Duration, Instant};

/// 消息条目(Rust 端记录)
struct MessageEntry {
    create_time: Instant,
    /// 总显示时长(毫秒)
    duration_ms: u64,
}

fn main() {
    let ui = MainWindow::new().expect("无法创建主窗口");

    // ── 消息模型 ──
    let model: Rc<VecModel<MessageItem>> = Rc::new(VecModel::from(vec![]));
    ui.set_messages(ModelRc::new(model.clone()));

    // ── 内部状态 ──
    let entries: Rc<RefCell<Vec<MessageEntry>>> = Rc::new(RefCell::new(Vec::new()));
    let counter = Rc::new(std::cell::Cell::new(0_i32));

    // ── 注册 add-message 回调 ──
    {
        let model = model.clone();
        let entries = entries.clone();
        let counter = counter.clone();

        ui.on_add_message(move |msg_type, title, content| {
            let id = counter.get();
            counter.set(id + 1);

            let item = MessageItem {
                id,
                r#type: msg_type,
                title,
                content,
                progress: 0.0,
                removing: false,
            };

            model.push(item);
            entries.borrow_mut().push(MessageEntry {
                create_time: Instant::now(),
                duration_ms: 5000,
            });
        });
    }

    // ── 注册 dismiss-message 回调(手动关闭) ──
    {
        let model = model.clone();
        let entries = entries.clone();

        ui.on_dismiss_message(move |id| {
            // 先标记为 removing(触发出场动画)
            let entries_ref = entries.borrow();
            if let Some(idx) = model.iter().position(|m| m.id == id) {
                let mut item = model.row_data(idx).unwrap();
                item.removing = true;
                model.set_row_data(idx, item);
            }
            drop(entries_ref);

            // 300ms 后真正移除(等动画完成)
            let model_clone = model.clone();
            let entries_clone = entries.clone();
            slint::Timer::single_shot(Duration::from_millis(350), move || {
                if let Some(idx) = model_clone.iter().position(|m| m.id == id) {
                    model_clone.remove(idx);
                    entries_clone.borrow_mut().remove(idx);
                }
            });
        });
    }

    // ── 进度更新定时器:每 50ms 更新一次 ──
    {
        let model = model.clone();
        let entries = entries.clone();

        let progress_timer = Timer::default();
        progress_timer.start(
            TimerMode::Repeated,
            Duration::from_millis(50),
            move || {
                let entries_ref = entries.borrow();
                let now = Instant::now();

                for (idx, entry) in entries_ref.iter().enumerate() {
                    let elapsed = now.duration_since(entry.create_time).as_millis() as f64;
                    let progress = (elapsed / entry.duration_ms as f64).min(1.0);

                    if let Some(mut item) = model.row_data(idx) {
                        if (item.progress - progress as f32).abs() > 0.01 {
                            item.progress = progress as f32;
                            model.set_row_data(idx, item);
                        }
                    }
                }
            },
        );
        std::mem::forget(progress_timer);
    }

    // ── 自动移除定时器:每秒检查 ──
    {
        let model = model.clone();
        let entries = entries.clone();

        let cleanup_timer = Timer::default();
        cleanup_timer.start(
            TimerMode::Repeated,
            Duration::from_millis(100),
            move || {
                let entries_ref = entries.borrow();
                let now = Instant::now();

                // 找出已过期的消息
                let expired: Vec<(i32, usize)> = entries_ref
                    .iter()
                    .enumerate()
                    .filter(|(_, entry)| {
                        now.duration_since(entry.create_time).as_millis() as u64 >= entry.duration_ms
                    })
                    .map(|(idx, entry)| (entry.create_time.elapsed().as_millis() as i32, idx))
                    .collect();

                drop(entries_ref);

                // 标记为 removing(触发出场动画)
                for (_, idx) in &expired {
                    if let Some(mut item) = model.row_data(*idx) {
                        if !item.removing {
                            item.removing = true;
                            model.set_row_data(*idx, item);
                        }
                    }
                }

                // 延迟移除已过期的(等动画)
                if !expired.is_empty() {
                    let model_clone = model.clone();
                    let entries_clone = entries.clone();
                    let indices: Vec<usize> = expired.iter().map(|(_, idx)| *idx).collect();

                    slint::Timer::single_shot(Duration::from_millis(350), move || {
                        let mut entries_mut = entries_clone.borrow_mut();
                        // 从高到低删除,避免索引偏移
                        for &idx in indices.iter().rev() {
                            if idx < model_clone.row_count() {
                                model_clone.remove(idx);
                                if idx < entries_mut.len() {
                                    entries_mut.remove(idx);
                                }
                            }
                        }
                    });
                }
            },
        );
        std::mem::forget(cleanup_timer);
    }

    ui.run().expect("运行 UI 失败");
}

3、build.rs

rust 复制代码
fn main(){
    slint_build::compile("ui/main.slint")
        .unwrap()
}

4、main.slint

rust 复制代码
import { Button, VerticalBox, HorizontalBox, LineEdit, ScrollView } from "std-widgets.slint";

// 消息类型枚举
export enum MessageType {
    success,
    error,
    warning,
    info
}

// 消息数据结构
export struct MessageItem {
    id: int,
    type: MessageType,
    title: string,
    content: string,
    // 进度值 0.0 ~ 1.0
    progress: float,
    // 是否正在移除(触发退出动画)
    removing: bool,
}

// 颜色常量
global MessageTheme {
    // 成功 - 绿色
    out property <color> success-color: #10b981;
    out property <color> success-bg: #ecfdf5;

    // 错误 - 红色
    out property <color> error-color: #ef4444;
    out property <color> error-bg: #fef2f2;

    // 警告 - 橙色
    out property <color> warning-color: #f59e0b;
    out property <color> warning-bg: #fffbeb;

    // 信息 - 蓝色
    out property <color> info-color: #3b82f6;
    out property <color> info-bg: #eff6ff;
}

// 可复用按钮组件
component MsgButton inherits Rectangle {
    in property <image> icon-src;
    in property <string> label;
    in property <color> accent;
    callback clicked();

    height: 50px;
    width: 130px;
    border-radius: 25px;
    border-width: 2px;
    border-color: accent;
    background: accent;
    opacity: area.has-hover ? 0.8 : 1.0;

    HorizontalBox {
        padding: 12px;
        alignment: center;
        spacing: 8px;

        Rectangle {
            Image {
                width: 16px;
                height: 16px;
                vertical-stretch: 0;
                source: icon-src;
                colorize: white;
            }
        }


        Text {
            text: label;
            color: white;
            font-size: 16px;
            font-weight: 600;
            vertical-stretch: 0;
            vertical-alignment: center;
        }
    }

    area := TouchArea {
        width: 100%;
        height: 100%;
        clicked => { root.clicked(); }
        mouse-cursor: pointer;
    }
}

// 单条消息组件
component MessageCard inherits Rectangle {
    in property <MessageItem> message;
    callback dismiss(int);

    width: 380px;
    height: 80px;
    border-radius: 12px;

    // 根据消息类型获取主题色
    property <color> accent: message.type == MessageType.success ? MessageTheme.success-color :
                             message.type == MessageType.error ? MessageTheme.error-color :
                             message.type == MessageType.warning ? MessageTheme.warning-color :
                             MessageTheme.info-color;

    property <color> bg: message.type == MessageType.success ? MessageTheme.success-bg :
                         message.type == MessageType.error ? MessageTheme.error-bg :
                         message.type == MessageType.warning ? MessageTheme.warning-bg :
                         MessageTheme.info-bg;

    // 白色圆角背景(问题2修复:使用更深的边框颜色,确保圆角可见)
    background: bg;
    border-width: 2px;
    border-color: accent;

    // 入场/出场动画
    animate x { duration: 400ms; easing: cubic-bezier(0.34, 1.56, 0.64, 1); }
    animate opacity { duration: 300ms; easing: ease-in-out; }
    opacity: message.removing ? 0.0 : 1.0;

    // 图标区域
    Rectangle {
        x: 12px;
        y: 16px;
        width: 48px;
        height: 48px;
        border-radius: 24px;
        background: accent;

        // 使用 SVG 图标(问题1修复:用图片替代文字)
        Image {
            x: 12px;
            y: 12px;
            width: 24px;
            height: 24px;
            source: message.type == MessageType.success ? @image-url("../images/icon-success.svg") :
                    message.type == MessageType.error ? @image-url("../images/icon-error.svg") :
                    message.type == MessageType.warning ? @image-url("../images/icon-warning.svg") :
                    @image-url("../images/icon-info.svg");
        }
    }

    // 文字内容区域
    VerticalBox {
        x: 72px;
        y: 12px;
        width: parent.width - 108px;
        height: 56px;

        Text {
            text: message.title;
            font-size: 15px;
            font-weight: 600;
            color: #1f2937;
            overflow: elide;
        }

        Text {
            text: message.content;
            font-size: 13px;
            color: #6b7280;
            overflow: elide;
        }
    }

    // 关闭按钮
    Rectangle {
        x: parent.width - 32px;
        y: 8px;
        width: 24px;
        height: 24px;
        border-radius: 12px;
        property <bool> touched: close-btn.has-hover;
        background: touched ? #00000015 : transparent;

        Text {
            text: "x";
            font-size: 14px;
            font-weight: 700;
            color: touched ? #1f2937 : #9ca3af;
            horizontal-alignment: center;
            vertical-alignment: center;
        }

        close-btn := TouchArea {
            width: 100%;
            height: 100%;
            clicked => { dismiss(message.id); }
            mouse-cursor: pointer;
        }
    }

    // ======== 进度条 ========
    Rectangle {
        x: 12px;
        y: root.height - 6px;
        width: (root.width - 24px) * (1.0 - message.progress);
        height: 3px;
        border-radius: 1.5px;
        background: accent;
        animate width { duration: 100ms; easing: linear; }
    }
}

export component MainWindow inherits Window {
    title: "高级消息提示框";
    preferred-width: 1000px;
    preferred-height: 700px;

    // 消息列表
    in property <[MessageItem]> messages:[
        { id: 0, type: MessageType.success, title: "操作成功", content: "您的更改已保存并同步到云端。", progress: 0.0, removing: false },
        { id: 1, type: MessageType.error, title: "操作失败", content: "由于网络问题,您的更改未能保存。", progress: 0.0, removing: false },
        { id: 2, type: MessageType.warning, title: "警告", content: "您的操作可能存在风险,请谨慎处理。", progress: 0.0, removing: false },
    ];

    // 回调
    callback add-message(MessageType, string, string);
    callback dismiss-message(int);

    // 渐变背景
    background: @linear-gradient(135deg, #667eea 0%, #764ba2 100%);

    // 主布局
    VerticalBox {
        padding: 40px;

        // 标题
        Text {
          //  text: "高级消息提示框演示";
            font-size: 32px;
            font-weight: 700;
            color: white;
            horizontal-alignment: center;
        }

        Rectangle { height: 20px; }

        // 控制区域
        Rectangle {
            height: 120px;
            border-radius: 16px;
            background: #ffffff1a;
            clip: true;

            // 毛玻璃效果模拟
            border-width: 1px;
            border-color: #ffffff33;

            HorizontalBox {
                padding: 20px;
                alignment: space-around;

                MsgButton {
                    icon-src: @image-url("../images/btn-success.svg");
                    label: "成功";
                    accent: #10b981;
                    clicked => { add-message(MessageType.success, "操作成功", "您的更改已保存并同步到云端。"); }
                }

                MsgButton {
                    icon-src: @image-url("../images/btn-error.svg");
                    label: "错误";
                    accent: #ef4444;
                    clicked => { add-message(MessageType.error, "发生错误", "无法连接到服务器,请检查网络。"); }
                }

                MsgButton {
                    icon-src: @image-url("../images/btn-warning.svg");
                    label: "警告";
                    accent: #f59e0b;
                    clicked => { add-message(MessageType.warning, "注意", "存储空间即将用尽,请及时清理。"); }
                }

                MsgButton {
                    icon-src: @image-url("../images/btn-info.svg");
                    label: "信息";
                    accent: #3b82f6;
                    clicked => { add-message(MessageType.info, "系统提示", "新版本 v2.0 已发布,点击查看详情。"); }
                }
            }
        }

        Rectangle { height: 30px; }

    }

    // 消息提示框容器(右上角,带滚动)
    sv := ScrollView {
        x: parent.width - 400px;
        y: 20px;
        width: 380px;
        height: parent.height - 40px;

        // 自动滚动到底部
        viewport-y: sv.visible-height - sv.viewport-height;
        vertical-scrollbar-policy: always-off;

        VerticalLayout {
            spacing: 12px;
            alignment: start;

            for msg in messages : MessageCard {
                message: msg;
                dismiss(id) => { dismiss-message(id); }
            }
        }
    }
}

5、Cargo.toml

rust 复制代码
[package]
name = "modbus-debug-assistant"
version = "0.1.0"
edition = "2024"

[dependencies]
slint = "1.17.1"


[build-dependencies]
slint-build = "1.17.1"

三、实现原理

本项目基于 Rust + Slint 构建,核心思路是用 Rust 管理业务逻辑与数据模型,用 Slint 负责界面渲染与动画,两者通过 Slint 自动生成的类型安全接口进行通信。整体实现可分为以下几个关键部分:

1、数据模型与类型绑定

main.slint 中定义了 MessageItem 结构体,包含 idtypetitlecontentprogressremoving 六个字段,分别用于标识消息的唯一编号、类型、标题、内容、进度值以及是否正在移除。同时通过 export enum MessageType 定义了 successerrorwarninginfo 四种消息类型。

Rust 端通过 slint::include_modules!() 宏将 Slint 编译生成的代码引入,从而可以直接使用 MainWindowMessageItem 等类型。借助 VecModel<MessageItem> 作为消息列表的数据模型,并通过 ui.set_messages(ModelRc::new(model.clone())) 将模型绑定到界面,实现 Rust 与 Slint 之间的数据共享。

2、消息的添加与回调机制

界面上的四个按钮(成功、错误、警告、信息)通过 clicked 信号触发 add-message 回调。Rust 端通过 ui.on_add_message(...) 注册回调闭包,接收消息类型、标题和内容三个参数:

  • 使用 counter 自增生成消息的唯一 id
  • 构造 MessageItempushVecModel 中;
  • 同时在 entries 向量中记录该消息的创建时间 Instant::now() 和显示时长 duration_ms(这里设为 5000ms)。

由于 VecModel 是响应式的,push 操作会立即触发 Slint 界面的 for msg in messages 循环重新求值,从而自动渲染出新的消息卡片。

3、进度条更新机制

进度条用于展示消息的剩余显示时间。Rust 端创建了一个 Timer,以 50ms 为周期重复触发:

  • 遍历 entries 中所有消息,计算当前时间与创建时间的差值 elapsed
  • elapsed / duration_ms 得到 0.0 ~ 1.0 的进度值 progress
  • 当进度变化超过 0.01 时,通过 model.set_row_data(idx, item) 更新对应消息的 progress 字段。

Slint 端进度条通过 width: (root.width - 24px) * (1.0 - message.progress) 绑定进度值,并配合 animate width { duration: 100ms; } 实现平滑的进度条收缩动画。

4、消息的自动移除与出场动画

当消息的存活时间达到 duration_ms 后,自动移除定时器(每 100ms 检查一次)会将其标记为过期:

  1. 先找出所有已过期的消息索引;
  2. 将对应消息的 removing 字段设为 true,触发 Slint 端的出场动画(opacity 渐隐 + x 位移动画);
  3. 等待 350ms(动画时长)后,通过 Timer::single_shot 延迟移除该消息,从高到低删除索引以避免偏移。

手动关闭的逻辑类似:用户点击消息卡片的关闭按钮时,触发 dismiss-message 回调,同样先标记 removing 再延迟移除,保证出场动画完整播放。

5、界面渲染与动画实现

Slint 端通过声明式语法完成界面布局与动画:

  • 消息卡片 MessageCard 根据 message.type 动态选择主题色(成功绿、错误红、警告橙、信息蓝);
  • 入场动画 :卡片默认从右侧滑入,通过 animate xanimate opacity 实现弹性缓动效果;
  • 出场动画 :当 removingtrue 时,卡片透明度渐变为 0 并向右滑出;
  • 滚动容器 :消息列表放在 ScrollView 中,并通过 viewport-y: sv.visible-height - sv.viewport-height 实现自动滚动到底部,保证新消息始终可见。

6、整体流程

#mermaid-svg-pyGYpGhzddk5yLgh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pyGYpGhzddk5yLgh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pyGYpGhzddk5yLgh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pyGYpGhzddk5yLgh .error-icon{fill:#552222;}#mermaid-svg-pyGYpGhzddk5yLgh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pyGYpGhzddk5yLgh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pyGYpGhzddk5yLgh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pyGYpGhzddk5yLgh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pyGYpGhzddk5yLgh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pyGYpGhzddk5yLgh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pyGYpGhzddk5yLgh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pyGYpGhzddk5yLgh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pyGYpGhzddk5yLgh .marker.cross{stroke:#333333;}#mermaid-svg-pyGYpGhzddk5yLgh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pyGYpGhzddk5yLgh p{margin:0;}#mermaid-svg-pyGYpGhzddk5yLgh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pyGYpGhzddk5yLgh .cluster-label text{fill:#333;}#mermaid-svg-pyGYpGhzddk5yLgh .cluster-label span{color:#333;}#mermaid-svg-pyGYpGhzddk5yLgh .cluster-label span p{background-color:transparent;}#mermaid-svg-pyGYpGhzddk5yLgh .label text,#mermaid-svg-pyGYpGhzddk5yLgh span{fill:#333;color:#333;}#mermaid-svg-pyGYpGhzddk5yLgh .node rect,#mermaid-svg-pyGYpGhzddk5yLgh .node circle,#mermaid-svg-pyGYpGhzddk5yLgh .node ellipse,#mermaid-svg-pyGYpGhzddk5yLgh .node polygon,#mermaid-svg-pyGYpGhzddk5yLgh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pyGYpGhzddk5yLgh .rough-node .label text,#mermaid-svg-pyGYpGhzddk5yLgh .node .label text,#mermaid-svg-pyGYpGhzddk5yLgh .image-shape .label,#mermaid-svg-pyGYpGhzddk5yLgh .icon-shape .label{text-anchor:middle;}#mermaid-svg-pyGYpGhzddk5yLgh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pyGYpGhzddk5yLgh .rough-node .label,#mermaid-svg-pyGYpGhzddk5yLgh .node .label,#mermaid-svg-pyGYpGhzddk5yLgh .image-shape .label,#mermaid-svg-pyGYpGhzddk5yLgh .icon-shape .label{text-align:center;}#mermaid-svg-pyGYpGhzddk5yLgh .node.clickable{cursor:pointer;}#mermaid-svg-pyGYpGhzddk5yLgh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pyGYpGhzddk5yLgh .arrowheadPath{fill:#333333;}#mermaid-svg-pyGYpGhzddk5yLgh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pyGYpGhzddk5yLgh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pyGYpGhzddk5yLgh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pyGYpGhzddk5yLgh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pyGYpGhzddk5yLgh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pyGYpGhzddk5yLgh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pyGYpGhzddk5yLgh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pyGYpGhzddk5yLgh .cluster text{fill:#333;}#mermaid-svg-pyGYpGhzddk5yLgh .cluster span{color:#333;}#mermaid-svg-pyGYpGhzddk5yLgh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pyGYpGhzddk5yLgh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pyGYpGhzddk5yLgh rect.text{fill:none;stroke-width:0;}#mermaid-svg-pyGYpGhzddk5yLgh .icon-shape,#mermaid-svg-pyGYpGhzddk5yLgh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pyGYpGhzddk5yLgh .icon-shape p,#mermaid-svg-pyGYpGhzddk5yLgh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pyGYpGhzddk5yLgh .icon-shape .label rect,#mermaid-svg-pyGYpGhzddk5yLgh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pyGYpGhzddk5yLgh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pyGYpGhzddk5yLgh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pyGYpGhzddk5yLgh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否

用户点击按钮
触发 add-message 回调
Rust 构造 MessageItem 并 push 到 VecModel
Slint 自动渲染消息卡片
进度定时器每 50ms 更新 progress
进度条宽度实时收缩
存活时间 >= 5000ms?
标记 removing = true
触发出场动画(渐隐 + 位移)
350ms 后从模型中移除

通过以上机制,Rust 负责数据与定时逻辑,Slint 负责渲染与动画,两者各司其职、通过类型安全的绑定高效协作,最终实现了流畅的动态消息提示效果。

相关推荐
小奏技术1 小时前
10 MB 的 Postman 替代品,启动不到 1 秒
后端
东风破_1 小时前
Text2SQL :用自然语言操作 SQLite 数据库
人工智能·后端
传奇开心果编程3 小时前
【Rust入门知识点学与练】第21课:Trait 进阶 Advanced Traits
开发语言·学习·rust
新时代牛马3 小时前
字符设备驱动完整篇:从 cdev_add、file_operations 到chrdev_open 与排障
开发语言·python
swordbob3 小时前
ReentrantLock 与 AQS 完整学习手册
java·开发语言
白山编程大哥4 小时前
Java OutputStreamWriter 详解:从字符到字节的桥梁
java·开发语言·python
shmily麻瓜小菜鸡5 小时前
JavaScript / TypeScript 易踩坑知识点 —— 异步编程类
开发语言·javascript·typescript
七夜zippoe5 小时前
为什么 2026 年每个 Java 团队都该懂 AI Agent
java·开发语言·人工智能
znnnk5 小时前
【Python】GUI 开发从入门到实战(三):PyQt/PySide 进阶之路
开发语言·python·pyqt