C#电话簿应用开发完整源码项目实战
简介:本文详细讲解如何使用C#语言在Visual Studio 2010环境中开发一个功能完整的电话簿应用程序。项目基于面向对象编程思想,设计Contact和Phonebook类实现联系人管理核心逻辑,涵盖添加、编辑、查找、删除联系人等功能,并通过Windows Forms构建直观的用户界面。系统支持数据持久化,利用文件操作将联系人信息以XML或JSON格式保存,确保程序重启后数据不丢失。同时集成输入验证、错误提示和搜索匹配机制,提升用户体验。本项目是掌握C#基础语法、UI设计与数据处理综合能力的理想实践案例。
C#电话簿应用:从零构建一个现代化桌面管理系统
你有没有过这样的经历?手机里存了几百个联系人,想找某个朋友时却翻了半天也找不到。或者更糟——换了一台新设备,所有号码都得重新录入一遍?😱 这种“数字失忆症”在今天这个信息爆炸的时代太常见了。而我们即将要打造的,就是一个能帮你彻底告别这种烦恼的小工具: C#电话簿管理系统 。
但这不仅仅是一个简单的“增删改查”练习项目。我们要用现代软件工程的思维,把面向对象设计、事件驱动架构、数据持久化和UI交互这些关键技术点揉在一起,像搭积木一样,亲手组装出一个真正可用、可维护、还能不断进化的桌面应用。
准备好了吗?让我们从最基础的地方开始——不是直接写代码,而是先问自己一个问题: 为什么我们需要“类”?
🧱 从现实世界到程序世界的映射: Contact 类的设计哲学
想象一下你在纸上记下一个联系人:
张三
手机:13800138000
邮箱:zhangsan@example.com
这三条信息属于同一个人,它们天然地“绑在一起”。如果我们用三个独立变量来存储( string name , string phone , string email ),那系统很快就会变得混乱不堪——你怎么知道哪个名字对应哪个电话?
这就是 类(Class) 存在的意义:它是一种将相关数据与行为组织在一起的方式。我们可以这样定义一个最基本的 Contact 类:
public class Contact
{
public string Name { get; set; }
public string PhoneNumber { get; set; }
}
短短几行代码,就已经完成了一次“现实 → 程序”的抽象转换。但别急着运行!这段代码虽然简洁,却藏着不少隐患。
比如,如果用户传进来的是空字符串甚至 null 值怎么办?难道我们要让整个系统因为一条无效数据就崩溃吗?显然不行。所以真正的生产级设计必须考虑更多细节。
✅ 属性封装的艺术:自动属性 vs 显式字段
C# 提供了非常优雅的 自动属性语法 ,它可以自动生成背后的私有字段(backing field),让你少写很多样板代码:
public string Email { get; set; } // 编译器会生成 private string <Email>k__BackingField
但对于一些关键字段,比如电话号码,我们可能需要加入校验逻辑。这时候就不能再偷懒了,得手动控制 set 访问器:
private string _phoneNumber;
public string PhoneNumber
{
get => _phoneNumber;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("电话号码不能为空");
if (!IsValidPhoneNumber(value))
throw new ArgumentException("电话号码格式不合法");
_phoneNumber = value.Trim();
}
}
💡 小贴士:
_phoneNumber是私有字段,外面无法直接访问;而PhoneNumber是公共属性,提供了受控的数据入口。这种模式叫“封装”,是 OOP 的核心思想之一。
那么问题来了:怎么判断手机号是否合法呢?中国的手机号通常是11位,以1开头,第二位是3-9之间的数字。我们可以用正则表达式来验证:
private static readonly Regex PhoneRegex =
new Regex(@"^1[3-9]\d{9}$", RegexOptions.Compiled);
private bool IsValidPhoneNumber(string number)
{
return !string.IsNullOrWhiteSpace(number) && PhoneRegex.IsMatch(number);
}
注意到我用了 RegexOptions.Compiled 吗?这是为了提升性能。虽然首次匹配稍慢一点,但后续调用会更快,特别适合频繁使用的场景。
🔁 构造函数重载:让对象创建更灵活
当你新建一个联系人时,是不是每次都要填完所有信息才能保存?现实中往往不是这样。有时候你知道名字和电话就够了,邮箱可以以后补上。
这就引出了 构造函数重载(Constructor Overloading) 的概念——提供多种方式来初始化对象:
// 默认构造函数(用于序列化)
public Contact() { }
// 全参数构造函数
public Contact(string name, string phoneNumber, string email = null, DateTime? birthDate = null)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
PhoneNumber = phoneNumber; // 自动触发校验逻辑
Email = email;
BirthDate = birthDate;
}
// 简化构造函数(仅姓名与电话)
public Contact(string name, string phoneNumber)
: this(name, phoneNumber, null, null) { }
看到最后一行的 : this(...) 了吗?这是一种叫 构造函数链式调用(constructor chaining) 的技巧。它避免了重复代码,也让逻辑更加清晰。
你可以这样使用:
var c1 = new Contact(); // 空对象
var c2 = new Contact("李四", "13900139000"); // 最常用场景
var c3 = new Contact("王五", "13700137000", "wangwu@demo.com", new DateTime(1990, 5, 20));
是不是比一个个赋值方便多了?
🎯 ToString() 方法重写:让对象“会说话”
如果你把上面任何一个 Contact 对象直接打印出来,比如:
Console.WriteLine(c2);
输出会是什么?你会看到类似 PhoneBookApp.Contact 这样的类型名。这对用户毫无意义!
解决办法就是重写 ToString() 方法:
public override string ToString()
{
var sb = new StringBuilder();
sb.Append($"{Name}");
if (!string.IsNullOrEmpty(PhoneNumber))
sb.Append($" ({PhoneNumber})");
if (!string.IsNullOrEmpty(Email))
sb.Append($", {Email}");
return sb.ToString();
}
现在再打印一次:
李四 (13900139000), lisi@example.com
清爽多了吧?😎 而且这个方法还会被 Windows Forms 的 ListView 控件自动调用,作为列表项的显示文本,一举两得!
📦 容器的力量: Phonebook 类如何管理成百上千个联系人
如果说 Contact 是砖块,那 Phonebook 就是整栋房子。它的职责不再是描述单个实体,而是组织、管理和操作一组 Contact 实例。
🗃️ 内部存储结构的选择:为什么是 List<Contact> ?
.NET 提供了好几种集合类型: Array , List<T> , LinkedList<T> , SortedSet<T> ……我们该怎么选?
对于电话簿这种应用场景, List<T> 是最佳选择,原因如下:
| 特性 | 说明 |
|---|---|
| 动态扩容 | 不需要预估容量大小,插入元素时自动增长 |
| 随机访问快 | 支持 O(1) 时间复杂度索引访问 |
| API 丰富 | 内置 Add , Remove , Find , Where 等实用方法 |
来看具体实现:
public class Phonebook
{
private readonly List<Contact> _contacts;
public Phonebook()
{
_contacts = new List<Contact>();
}
public IReadOnlyList<Contact> Contacts => _contacts.AsReadOnly();
}
注意这里用了两个重要设计:
_contacts字段加了readonly,确保一旦初始化后不能再指向其他集合;- 暴露的是
IReadOnlyList<Contact>接口,而不是原始的List<Contact>,防止外部误删或修改内部状态。
⚠️ 经验之谈:永远不要暴露可变集合给外界!否则别人一句
_phonebook.Contacts.Clear()就能把你的数据全清空了。
➕ 添加联系人:不只是 .Add() 那么简单
你以为添加联系人就是调用 list.Add(contact) ?Too young too simple!
真实世界中,我们得考虑几个关键问题:
- 输入为空怎么办?
- 重复添加同一个联系人会不会出错?
- 添加成功后要不要通知其他模块?
所以我们需要更健壮的方法:
public bool AddContact(Contact contact)
{
if (contact == null)
throw new ArgumentNullException(nameof(contact));
if (_contacts.Any(c =>
c.Name == contact.Name && c.PhoneNumber == contact.PhoneNumber))
return false; // 已存在相同联系人
_contacts.Add(contact);
OnContactAdded?.Invoke(this, new ContactEventArgs(contact));
return true;
}
// 事件定义
public event EventHandler<ContactEventArgs> OnContactAdded;
看到了吗?我们在添加前做了去重检查,并通过 事件机制 发出通知。这意味着 UI 层可以监听这个事件,自动刷新列表;日志模块也可以记录操作历史。
📢 什么叫“松耦合”?这就是!
Phonebook根本不知道谁在听它的广播,但它依然能把消息传出去。
顺便提一句,这里的去重是基于“姓名+电话”组合判断的。如果你希望更精确,可以用 GUID 作为唯一标识符:
public class Contact
{
public Guid Id { get; } = Guid.NewGuid();
// ...
}
然后比较 Id 是否相等即可。
❌ 删除联系人:小心 O(n) 性能陷阱
删除操作看起来也很简单:
public bool RemoveContact(string name, string phoneNumber)
{
var contact = _contacts.FirstOrDefault(c =>
c.Name.Equals(name, StringComparison.OrdinalIgnoreCase) &&
c.PhoneNumber == phoneNumber);
if (contact != null)
{
_contacts.Remove(contact);
OnContactRemoved?.Invoke(this, new ContactEventArgs(contact));
return true;
}
return false;
}
但请注意: .FirstOrDefault() 和 .Remove() 都是线性查找,时间复杂度为 O(n)。当联系人数量达到几千条时,响应速度会明显变慢。
优化方案?引入哈希索引!
private Dictionary<string, Contact> _indexByName;
private void RebuildIndex()
{
_indexByName = _contacts.ToDictionary(
c => c.Name.ToLowerInvariant(),
c => c,
StringComparer.OrdinalIgnoreCase);
}
这样查询就能降到 O(1),代价是占用更多内存。典型的“空间换时间”。
✏️ 修改联系人:引用类型的魔法
由于 Contact 是引用类型,修改它的属性不会影响其在集合中的位置:
public bool UpdateContact(string oldName, string oldPhone, Contact updatedContact)
{
var existing = _contacts.FirstOrDefault(c =>
c.Name == oldName && c.PhoneNumber == oldPhone);
if (existing == null || updatedContact == null) return false;
existing.Email = updatedContact.Email;
existing.BirthDate = updatedContact.BirthDate;
OnContactUpdated?.Invoke(this, new ContactEventArgs(existing));
return true;
}
你看,我们并没有从集合中移除旧对象、再插入新对象,而是直接修改原对象的字段。这不仅节省内存,还保持了对象身份的一致性。
不过这也带来一个问题:如果用户点了“取消编辑”,你怎么恢复原来的值?答案是——你需要提前保存一份快照(snapshot),或者实现撤销栈(undo stack)。这是进阶功能,后面有机会再讲。
🖼️ 用户界面长什么样?用 WinForms 搭建直观的操作面板
有了后台逻辑,接下来就是让用户看得见、摸得着的前端界面了。我们选用 Windows Forms ,因为它足够轻量、学习成本低,而且非常适合这类小型桌面应用。
🏗️ 主窗体布局:从零开始搭建 UI 框架
每个 WinForms 应用都有一个主窗体(Form),它是所有控件的容器。我们先做些基本设置:
public partial class MainForm : Form
{
private Phonebook _phonebook;
public MainForm()
{
InitializeComponent();
// 设置窗口样式
this.Text = "C#电话簿管理系统";
this.StartPosition = FormStartPosition.CenterScreen;
this.FormBorderStyle = FormBorderStyle.FixedSingle;
this.MaximizeBox = false;
this.Size = new Size(800, 600);
_phonebook = new Phonebook();
}
}
这些配置看似琐碎,其实都很讲究:
- 居中启动 (
CenterScreen) —— 第一印象很重要; - 固定边框 + 禁止最大化 —— 防止布局错乱;
- 合适的初始尺寸 —— 兼顾小屏和大屏体验。
📝 输入区域:TextBox 与 Label 的黄金搭档
我们需要让用户输入姓名和电话。最常用的控件是 TextBox ,搭配 Label 作为提示文字:
private TextBox txtName;
private TextBox txtPhone;
private void InitializeInputControls()
{
txtName = new TextBox { Location = new Point(100, 30), Size = new Size(200, 30) };
txtPhone = new TextBox { Location = new Point(100, 70), Size = new Size(200, 30) };
var lblName = new Label { Text = "姓名:", Location = new Point(30, 35) };
var lblPhone = new Label { Text = "电话:", Location = new Point(30, 75) };
this.Controls.AddRange(new Control[] { txtName, txtPhone, lblName, lblPhone });
}
虽然 Visual Studio 提供了可视化设计器,但了解底层代码有助于你更好地掌控布局逻辑。
⚠️ 注意:原生
TextBox不支持占位符(placeholder)。如果你想实现“请输入姓名”这种灰色提示文字,需要继承TextBox自定义控件,或监听Enter/Leave事件动态切换文本颜色。
🎯 功能按钮:绿色添加、红色删除的视觉语义
按钮是用户操作的核心入口。我们布置三个主要按钮:
private Button btnAdd;
private Button btnEdit;
private Button btnDelete;
private void InitializeButtons()
{
btnAdd = new Button
{
Text = "添加联系人",
Location = new Point(500, 25),
Size = new Size(120, 35),
BackColor = Color.Green,
ForeColor = Color.White
};
btnAdd.Click += BtnAdd_Click;
btnEdit = new Button { /* ... */ };
btnDelete = new Button { /* ... */ };
this.Controls.AddRange(new Control[] { btnAdd, btnEdit, btnDelete });
}
颜色是有语言的:
- 绿色 表示安全的正向操作(新增);
- 橙色 表示状态变更(编辑);
- 红色 警告高风险动作(删除);
这种色彩心理学的应用能显著降低用户的认知负担。
📋 ListView:展示联系人的“舞台”
要用表格形式展示联系人列表, ListView 是最合适的选择:
private ListView listViewContacts;
private void InitializeListView()
{
listViewContacts = new ListView
{
Location = new Point(30, 150),
Size = new Size(740, 380),
View = View.Details,
FullRowSelect = true,
GridLines = true,
AllowColumnReorder = false,
MultiSelect = false
};
listViewContacts.Columns.Add("ID", 50, HorizontalAlignment.Center);
listViewContacts.Columns.Add("姓名", 200, HorizontalAlignment.Left);
listViewContacts.Columns.Add("电话", 150, HorizontalAlignment.Left);
this.Controls.Add(listViewContacts);
}
关键属性解释:
FullRowSelect:点击任意列都能选中整行,提升易用性;GridLines = true:加上网格线,数据更清晰;MultiSelect = false:只允许单选,避免误删多个条目。
至于数据填充,可以在每次添加/删除后调用刷新方法:
private void RefreshListView()
{
listViewContacts.Items.Clear();
foreach (var contact in _phonebook.Contacts)
{
var item = new ListViewItem(contact.Id.ToString());
item.SubItems.Add(contact.Name);
item.SubItems.Add(contact.Phone);
listViewContacts.Items.Add(item);
}
}
🔔 事件驱动编程:让按钮“活”起来
WinForms 是典型的 事件驱动框架 。什么意思?就是程序大部分时间都在“睡觉”,只有当用户点击、输入、滚动时才醒来干活。
⚡ Click 事件的三种绑定方式
Button.Click 事件本质是一个多播委托( EventHandler ),你可以用三种方式注册处理函数:
// 方式一:命名方法(推荐)
addButton.Click += AddButton_Click;
private void AddButton_Click(object sender, EventArgs e)
{
MessageBox.Show("添加成功!");
}
// 方式二:匿名方法
addButton.Click += delegate { MessageBox.Show("匿名方法触发"); };
// 方式三:Lambda 表达式(简洁)
addButton.Click += (s, e) => MessageBox.Show($"点击了{(s as Button)?.Text}");
三种各有优劣:
| 方式 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| 命名方法 | 易调试、可复用 | 略显冗长 | 复杂逻辑 |
| 匿名方法 | 内聚性强 | 不可解绑 | 一次性任务 |
| Lambda | 简洁 | 可能导致闭包内存泄漏 | 快速反馈 |
我个人建议优先使用第一种,除非逻辑极其简单。
🔍 “添加”按钮完整执行链路
完整的添加流程应该包含以下几个步骤:
- 获取输入值;
- 数据校验;
- 创建
Contact实例; - 添加到
Phonebook; - 刷新 UI;
- 清空输入框;
- 给用户反馈。
代码实现如下:
private void addButton_Click(object sender, EventArgs e)
{
try
{
string name = txtName.Text.Trim();
string phone = txtPhone.Text.Trim();
if (string.IsNullOrEmpty(name))
{
MessageBox.Show("姓名不能为空!", "输入错误", MessageBoxButtons.OK, MessageBoxIcon.Warning);
txtName.Focus();
return;
}
if (!IsValidPhone(phone))
{
MessageBox.Show("请输入有效的手机号码!", "格式错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
txtPhone.Focus();
return;
}
Contact newContact = new Contact(name, phone);
if (_phonebook.AddContact(newContact))
{
RefreshListView();
MessageBox.Show($"已成功添加联系人:{name}", "操作成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
ClearInputs();
}
else
{
MessageBox.Show("该联系人已存在!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
catch (Exception ex)
{
MessageBox.Show($"添加失败:{ex.Message}", "系统异常", MessageBoxButtons.OK, MessageBoxIcon.Stop);
}
}
每一步都有明确的目的和错误处理,这才是健壮的应用应有的样子。
🗑️ “删除”按钮的安全防护机制
删除操作具有破坏性,必须经过二次确认:
private void deleteButton_Click(object sender, EventArgs e)
{
if (listViewContacts.SelectedItems.Count == 0)
{
MessageBox.Show("请先选择要删除的联系人!", "无选择", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
ListViewItem selectedItem = listViewContacts.SelectedItems[0];
Contact contactToDelete = (Contact)selectedItem.Tag; // 注意:需提前设置 Tag
DialogResult result = MessageBox.Show(
$"确定要删除联系人【{contactToDelete.Name}】吗?\n电话:{contactToDelete.Phone}",
"确认删除",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question
);
if (result == DialogResult.Yes)
{
_phonebook.RemoveContact(contactToDelete);
RefreshListView();
MessageBox.Show("删除成功!", "完成", MessageBoxButtons.OK, MessageBoxIcon.None);
}
}
其中 Tag 属性非常有用——它允许你把任何对象附加到控件上。我们在填充 ListView 时就应该设置好:
item.Tag = contact;
这样才能反向查找到原始对象。
🔍 模糊搜索:快速定位海量联系人
随着联系人越来越多,手动翻找效率极低。我们需要一个模糊搜索功能。
🔤 基于姓名或号码的部分匹配
核心是 LINQ 查询:
private IEnumerable<Contact> SearchContacts(string keyword)
{
if (string.IsNullOrWhiteSpace(keyword))
return _phonebook.Contacts;
return _phonebook.Contacts.Where(c =>
c.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
c.PhoneNumber.Contains(keyword, StringComparison.OrdinalIgnoreCase)
);
}
StringComparison.OrdinalIgnoreCase 很关键,不然用户搜“张三”就找不到“zhangsan”。
还可以封装成扩展方法,方便复用:
public static class StringExtensions
{
public static bool ContainsIgnoreCase(this string source, string value)
{
return source?.Contains(value, StringComparison.OrdinalIgnoreCase) ?? false;
}
}
然后就可以这么写:
c.Name.ContainsIgnoreCase(keyword)
干净利落!
💾 数据持久化:关机后数据还在不在?
前面所有的努力都只是“临时记忆”。一旦程序关闭,数据就没了。我们必须让它学会“长期记忆”。
📁 文件路径规范:别再硬编码 C:\data...
新手常犯的错误是写死路径:
const string FilePath = "C:\\phonebook\\data.json"; // 危险!权限问题
正确的做法是动态获取程序目录:
public static class AppPaths
{
private static readonly string DataDir =
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data");
public static string ContactFile => Path.Combine(DataDir, "contacts.json");
static AppPaths()
{
if (!Directory.Exists(DataDir))
Directory.CreateDirectory(DataDir);
}
}
或者更符合 Windows 规范的做法,使用 %APPDATA% :
public static string UserDataPath =>
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"PhonebookApp", "contacts.json"
);
这样即使安装在 Program Files 下也能正常读写。
📦 JSON 序列化:轻量高效的首选方案
虽然 .NET 提供了 Binary、XML 等多种序列化方式,但现在最主流的是 JSON ,尤其是配合强大的 Newtonsoft.Json 库:
Install-Package Newtonsoft.Json
序列化代码极其简单:
public static void SaveToJson(List<Contact> contacts)
{
string json = JsonConvert.SerializeObject(contacts, Formatting.Indented);
File.WriteAllText(AppPaths.ContactFile, json);
}
public static List<Contact> LoadFromJson()
{
if (!File.Exists(AppPaths.ContactFile))
return new List<Contact>();
string json = File.ReadAllText(AppPaths.ContactFile);
return JsonConvert.DeserializeObject<List<Contact>>(json) ?? new List<Contact>();
}
生成的文件人类可读,便于调试:
[
{
"Id": "a1b2c3d4...",
"Name": "张三",
"PhoneNumber": "13800138000"
}
]
💡 提示:可以用
JsonSerializerSettings控制输出格式,比如忽略空值、驼峰命名等。
🔄 启动加载与退出保存:完整的生命周期闭环
最后一步,在程序启动时自动加载数据,关闭时自动保存:
public MainForm()
{
InitializeComponent();
LoadData(); // 启动时加载
RefreshListView();
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
try
{
SaveData(); // 关闭前保存
}
catch (Exception ex)
{
MessageBox.Show("保存失败:" + ex.Message);
}
}
从此,你的电话簿就有了“记忆”,再也不怕重启电脑啦!🎉
🛠️ 工程化实践:写出专业级代码的秘诀
最后聊聊那些让代码从“能跑”变成“好维护”的细节。
🧪 调试技巧:善用断点与输出窗口
- 在可疑代码行左侧点击设断点(红点);
- 按 F5 启动调试,程序会在断点处暂停;
- 用 F10/F11 逐行执行,观察变量变化;
- 查看“局部变量”窗口和“输出”窗口的日志。
还可以加跟踪日志:
Debug.WriteLine($"[Add] 正在添加联系人: {name}");
📁 项目结构组织:清晰的目录划分
建议结构如下:
/PhonebookApp
├── /Models (Contact.cs, Phonebook.cs)
├── /Views (MainForm.cs)
├── /Services (FileService.cs)
├── /Utils (StringExtensions.cs)
├── /Resources (图标、图片)
├── /Data (contacts.json)
└── Program.cs
命名空间也要统一:
namespace PhonebookApp.Models { ... }
🚀 发布部署:一键生成独立 exe 文件
发布时选择 Release 模式,配置为“单文件发布”+“自包含”:
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
输出目录里的 .exe 文件可以直接拷贝到其他电脑运行,无需安装 .NET 运行库。
🎉 结语:从小项目看大世界
这个电话簿看似简单,但我们已经触及了现代软件开发的多个核心领域:
- 面向对象设计 :封装、职责分离;
- 事件驱动模型 :UI 与业务解耦;
- 数据持久化 :JSON + 文件存储;
- 用户体验优化 :搜索、提示、布局;
- 工程化实践 :调试、发布、目录结构;
更重要的是,它具备良好的扩展性。未来你可以轻松加上:
- 导入/导出 CSV;
- 分组标签管理;
- 头像上传;
- 云同步;
- 夜间模式……
技术的成长,往往就藏在一个个小项目的不断迭代之中。🌟
所以,别再犹豫了——打开 Visual Studio,动手试试吧!说不定下一个改变世界的创意,就诞生于你敲下的第一行 new Contact() 中。💻✨
简介:本文详细讲解如何使用C#语言在Visual Studio 2010环境中开发一个功能完整的电话簿应用程序。项目基于面向对象编程思想,设计Contact和Phonebook类实现联系人管理核心逻辑,涵盖添加、编辑、查找、删除联系人等功能,并通过Windows Forms构建直观的用户界面。系统支持数据持久化,利用文件操作将联系人信息以XML或JSON格式保存,确保程序重启后数据不丢失。同时集成输入验证、错误提示和搜索匹配机制,提升用户体验。本项目是掌握C#基础语法、UI设计与数据处理综合能力的理想实践案例。
更多推荐


所有评论(0)