首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 仿 QQ 面板摺疊選單實作完整教學

     編輯:關於C#

前言

QQ 風格的側邊摺疊面板是經典 UI 模式:主選單按鈕垂直排列,點擊展開子選單面板。本文完整實作 WinForms 版本,涵蓋動態建立控制項、佈局計算、展開/摺疊動畫、事件處理與擴充性設計。

核心類別設計


using System;
using System.Drawing;
using System.Windows.Forms;

public class QQPanel : Panel
{
    // 設定屬性
    public string[] MainModules { get; set; }
    public string[,] SubModules { get; set; }
    public int MainButtonHeight { get; set; } = 50;
    public int SubButtonHeight { get; set; } = 30;
    public Color MainBackColor { get; set; } = SystemColors.Control;
    public Color SubBackColor { get; set; } = Color.AliceBlue;
    public FlatStyle ButtonFlatStyle { get; set; } = FlatStyle.Flat;

    // 內部狀態
    private Button _expandedButton = null;
    private Panel _subPanel = null;

    public QQPanel()
    {
        this.BorderStyle = BorderStyle.FixedSingle;
    }

    public void Initialize()
    {
        this.Controls.Clear();
        _expandedButton = null;
        _subPanel = null;
        
        if (MainModules == null) return;
        
        for (int i = 0; i < MainModules.Length; i++)
        {
            CreateMainButton(i);
        }
    }

    private void CreateMainButton(int index)
    {
        var btn = new Button
        {
            Name = $"MainBtn_{index}",
            Text = MainModules[index],
            Width = this.Width,
            Height = MainButtonHeight,
            Top = MainButtonHeight * index,
            FlatStyle = ButtonFlatStyle,
            BackColor = MainBackColor,
            Tag = index // 儲存索引
        };
        
        btn.Click += MainButton_Click;
        this.Controls.Add(btn);
    }

    private void MainButton_Click(object sender, EventArgs e)
    {
        Button clickedBtn = (Button)sender;
        int index = (int)clickedBtn.Tag;
        
        // 若點擊已展開的按鈕,則摺疊
        if (_expandedButton == clickedBtn)
        {
            CollapseSubPanel();
            _expandedButton = null;
            return;
        }
        
        // 展開新的子面板
        ExpandSubPanel(clickedBtn, index);
    }

    private void ExpandSubPanel(Button mainBtn, int mainIndex)
    {
        CollapseSubPanel(); // 先關閉舊的
        
        _expandedButton = mainBtn;
        
        // 計算子面板位置與大小
        int subTop = MainButtonHeight * (mainIndex + 1);
        int subHeight = this.Height - MainButtonHeight * MainModules.Length;
        
        if (subHeight < SubButtonHeight * 3) subHeight = SubButtonHeight * 3; // 最小高度
        
        _subPanel = new Panel
        {
            Name = $"SubPanel_{mainIndex}",
            Top = subTop,
            Left = 0,
            Width = this.Width,
            Height = subHeight,
            BackColor = SubBackColor,
            BorderStyle = BorderStyle.FixedSingle
        };
        
        // 建立子按鈕
        int subCount = SubModules.GetLength(1);
        for (int j = 0; j < subCount; j++)
        {
            string subText = SubModules[mainIndex, j];
            if (string.IsNullOrEmpty(subText)) continue;
            
            var subBtn = new Button
            {
                Name = $"SubBtn_{mainIndex}_{j}",
                Text = subText,
                Top = SubButtonHeight * j,
                Left = 0,
                Width = this.Width,
                Height = SubButtonHeight,
                FlatStyle = ButtonFlatStyle,
                BackColor = SubBackColor,
                Tag = new { Main = mainIndex, Sub = j }
            };
            
            subBtn.Click += SubButton_Click;
            _subPanel.Controls.Add(subBtn);
        }
        
        this.Controls.Add(_subPanel);
        
        // 重新排列主按鈕位置 (被展開面板往下推)
        RearrangeMainButtons(mainIndex);
    }

    private void CollapseSubPanel()
    {
        if (_subPanel != null)
        {
            this.Controls.Remove(_subPanel);
            _subPanel.Dispose();
            _subPanel = null;
        }
        
        // 恢復主按鈕原始位置
        for (int i = 0; i < MainModules.Length; i++)
        {
            if (this.Controls[$"MainBtn_{i}"] is Button btn)
            {
                btn.Top = MainButtonHeight * i;
            }
        }
    }

    private void RearrangeMainButtons(int expandedIndex)
    {
        for (int i = 0; i < MainModules.Length; i++)
        {
            if (this.Controls[$"MainBtn_{i}"] is Button btn)
            {
                if (i <= expandedIndex)
                {
                    btn.Top = MainButtonHeight * i;
                }
                else
                {
                    // 往下推:原位置 + 子面板高度
                    btn.Top = MainButtonHeight * i + _subPanel.Height;
                }
            }
        }
    }

    private void SubButton_Click(object sender, EventArgs e)
    {
        Button btn = (Button)sender;
        dynamic tag = btn.Tag;
        int mainIdx = tag.Main;
        int subIdx = tag.Sub;
        
        // 觸發自訂事件
        OnSubItemClicked(mainIdx, subIdx, btn.Text);
    }

    // 對外事件
    public event EventHandler SubItemClicked;
    
    protected virtual void OnSubItemClicked(int mainIndex, int subIndex, string text)
    {
        SubItemClicked?.Invoke(this, new SubItemClickedEventArgs(mainIndex, subIndex, text));
    }
}

public class SubItemClickedEventArgs : EventArgs
{
    public int MainIndex { get; }
    public int SubIndex { get; }
    public string Text { get; }
    
    public SubItemClickedEventArgs(int main, int sub, string text)
    {
        MainIndex = main;
        SubIndex = sub;
        Text = text;
    }
}

表單使用範例


public partial class MainForm : Form
{
    private QQPanel qqPanel;

    public MainForm()
    {
        InitializeComponent();
        InitializeQQPanel();
    }

    private void InitializeQQPanel()
    {
        qqPanel = new QQPanel
        {
            Dock = DockStyle.Left,
            Width = 200,
            MainModules = new[] { "訊息", "聯絡人", "群組", "設定" },
            SubModules = new string[4, 3]
            {
                { "收件箱", "發件箱", "草稿箱" },
                { "好友列表", "陌生人", "黑名單" },
                { "我的群組", "建立群組", "群組搜尋" },
                { "個人設定", "系統設定", "關於" }
            }
        };
        
        qqPanel.SubItemClicked += QqPanel_SubItemClicked;
        qqPanel.Initialize();
        
        this.Controls.Add(qqPanel);
    }

    private void QqPanel_SubItemClicked(object sender, SubItemClickedEventArgs e)
    {
        MessageBox.Show($"點擊:{e.Text} (主選單:{e.MainIndex}, 子選單:{e.SubIndex})");
        // 這裡切換右側內容頁面
        SwitchContentPage(e.MainIndex, e.SubIndex);
    }

    private void SwitchContentPage(int main, int sub)
    {
        // 實作內容切換邏輯
    }
}

進階功能:動畫效果


using System.Windows.Forms;

public class AnimatedQQPanel : QQPanel
{
    private Timer _animationTimer;
    private int _targetHeight;
    private int _animationStep = 10;
    private bool _isExpanding;

    public AnimatedQQPanel()
    {
        _animationTimer = new Timer { Interval = 15 };
        _animationTimer.Tick += AnimationTimer_Tick;
    }

    private void ExpandSubPanel(Button mainBtn, int mainIndex)
    {
        // ... 建立 _subPanel 但 Height 設為 0 ...
        _subPanel.Height = 0;
        this.Controls.Add(_subPanel);
        
        _targetHeight = CalculateSubPanelHeight(mainIndex);
        _isExpanding = true;
        _animationTimer.Start();
    }

    private int CalculateSubPanelHeight(int mainIndex)
    {
        int count = 0;
        for (int j = 0; j < SubModules.GetLength(1); j++)
            if (!string.IsNullOrEmpty(SubModules[mainIndex, j])) count++;
        return count * SubButtonHeight;
    }

    private void AnimationTimer_Tick(object sender, EventArgs e)
    {
        if (_isExpanding)
        {
            _subPanel.Height += _animationStep;
            if (_subPanel.Height >= _targetHeight)
            {
                _subPanel.Height = _targetHeight;
                _animationTimer.Stop();
                RearrangeMainButtons(Array.IndexOf(MainModules, _expandedButton.Text));
            }
        }
        else
        {
            _subPanel.Height -= _animationStep;
            if (_subPanel.Height <= 0)
            {
                _subPanel.Height = 0;
                _animationTimer.Stop();
                CollapseSubPanel();
            }
        }
    }
}

資料驅動模式:從 JSON/XML 載入


// MenuConfig.json
{
  "Modules": [
    {
      "Id": "msg",
      "Text": "訊息",
      "Icon": "msg.png",
      "Children": [
        { "Id": "inbox", "Text": "收件箱", "Action": "ShowInbox" },
        { "Id": "sent", "Text": "發件箱", "Action": "ShowSent" }
      ]
    },
    {
      "Id": "contacts",
      "Text": "聯絡人",
      "Children": [
        { "Id": "friends", "Text": "好友列表" },
        { "Id": "strangers", "Text": "陌生人" }
      ]
    }
  ]
}

// 載入邏輯
public void LoadFromJson(string jsonPath)
{
    string json = File.ReadAllText(jsonPath);
    var config = JsonSerializer.Deserialize(json);
    
    MainModules = config.Modules.Select(m => m.Text).ToArray();
    SubModules = new string[config.Modules.Count, 
        config.Modules.Max(m => m.Children?.Count ?? 0)];
    
    for (int i = 0; i < config.Modules.Count; i++)
    {
        for (int j = 0; j < config.Modules[i].Children.Count; j++)
        {
            SubModules[i, j] = config.Modules[i].Children[j].Text;
        }
    }
    
    Initialize();
}

最佳實踐總結

面向建議
控制項建立動態建立而非設計器,支援資料驅動
佈局計算集中在 RearrangeMainButtons,避免分散邏輯
事件設計使用自訂 EventArgs 傳遞豐富資訊
記憶體管理摺疊時 Dispose 子面板與按鈕
擴充性預留 Tag、事件、虛擬方法供繼承覆寫
效能大量項目考慮虛擬化或分頁載入

現代化替代:WPF / WinUI 3

新專案建議使用 WPF 的 Expander 或 TreeView 配合 DataTemplate,或 WinUI 3 的 NavigationView:

  • 宣告式 XAML 定義 UI
  • 內建動畫、虛擬化、樣式系統
  • MVVM 資料綁定,邏輯與 UI 完全分離
  • 高 DPI、觸控、現代視覺原生支援

總結

QQ 面板核心在於「主按鈕陣列 + 動態子面板 + 位置重算」三大機制。封裝為可重用控制項 (QQPanel) 並暴露資料屬性與事件,即可實現資料驅動、高內聚低耦合的設計。動畫效果可透過 Timer 漸變 Height 實現。現代桌面開發建議遷移至 WPF/WinUI 獲得更豐富的 UI 能力與開發體驗。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved