為自己做一個(gè)簡(jiǎn)單記賬簿
每個(gè)月收到信用卡賬單時(shí),我總會(huì)又驚又惑。上個(gè)月怎么又花了那么多錢(qián)?看著每一筆出帳流水,猛抓頭皮卻怎么也記不起來(lái)這錢(qián)是用在了哪兒。痛定思痛,采取行動(dòng),我要記賬。作為一個(gè)信奉技術(shù)能改變世界的IT人,我理所當(dāng)然的在網(wǎng)上搜索各種電子記賬本。在線的記賬功能不敢用(怕被騷擾),一些單機(jī)記賬軟件提供的功能又不是我想要的。
與此同時(shí),最近空下來(lái)的時(shí)候,我在看SQLite方面的資料。SQLite的簡(jiǎn)潔、小巧讓我有些愛(ài)不釋手。就此決定給自己做個(gè)記賬本,用SQLite作為本地?cái)?shù)據(jù)引擎。
功能概述
我需要的記賬功能比較簡(jiǎn)單:
***、記錄每一筆消費(fèi),并可以添加需要的標(biāo)簽。當(dāng)我查看明細(xì)時(shí),能知道自己買(mǎi)了啥。
第二、對(duì)我來(lái)說(shuō),消費(fèi)只需要分成兩種:‘生活必需消費(fèi)’和‘享受消費(fèi)’。每周、每月可以看到這兩種消費(fèi)所占的比例、金額。
第三、能查看自己近6個(gè)月的消費(fèi)走勢(shì)。
根據(jù)這3點(diǎn)需求,我為自己度身定制了這款記賬工具。
圖1是記賬本的啟動(dòng)框。
程序?qū)?dòng)一個(gè)工作線程來(lái)檢查記賬程序路徑下是否已存在賬本數(shù)據(jù)庫(kù),若沒(méi)有則創(chuàng)建該數(shù)據(jù)庫(kù)和所需的表結(jié)構(gòu)。同時(shí)定時(shí)器將輪詢檢查結(jié)果。
(圖1)
圖2是記賬本的主界面。
很多其他記賬軟件把消費(fèi)分成餐飲,交通,買(mǎi)衣服……或者更細(xì)。一筆賬到底歸為哪一類(lèi)要想個(gè)半天,同時(shí)出的圖表復(fù)雜但又意義不大。
為自己做的賬本只有兩種消費(fèi)類(lèi)別,對(duì)應(yīng)兩個(gè)大按鈕,點(diǎn)擊即可進(jìn)入記賬界面。這兩種消費(fèi)所占的比例和總額是我每月的關(guān)注點(diǎn)。
主界面的最下方還有3個(gè)按鈕,分別對(duì)應(yīng)‘返回主界面’、‘退出程序’、‘查看報(bào)表’。在任何其它界面中,這三個(gè)按鈕的圖案、功能都保持一致。
(圖2)
點(diǎn)擊主界面上的綠色或紅色按鈕就會(huì)進(jìn)入到記賬界面。如圖3所示
標(biāo)題、圖標(biāo)、主色調(diào)區(qū)分了不同的消費(fèi)。該界面的設(shè)計(jì)也是希望最簡(jiǎn)化,省去了消費(fèi)時(shí)間選擇框,默認(rèn)為當(dāng)前記錄時(shí)間。
該界面的一個(gè)亮點(diǎn)是‘標(biāo)簽選擇框’??蛑械臉?biāo)簽是動(dòng)態(tài)生成的。系統(tǒng)會(huì)取近一個(gè)月時(shí)間,使用最頻繁的10個(gè)標(biāo)簽來(lái)顯示。(代碼分析部分還會(huì)展開(kāi))
這里記錄的標(biāo)簽,會(huì)出現(xiàn)在后面的明細(xì)報(bào)表中,這是我用來(lái)對(duì)賬的。
(圖3)
***來(lái)看一下這個(gè)小工具能生成的圖表與報(bào)表,如圖4所示
該工具能輸出3種報(bào)表,分別是消費(fèi)比例圖,近6月消費(fèi)走勢(shì)圖,消費(fèi)對(duì)賬明細(xì)。對(duì)于圖表,鼠標(biāo)至于色塊上方時(shí)將顯示消費(fèi)金額。
這3個(gè)報(bào)表也本著減少操作,降低復(fù)雜度,簡(jiǎn)潔好用為宗旨,所以只提供了最必要的功能。
(圖4)
程序結(jié)構(gòu)
看了工具的界面設(shè)計(jì)后,讓我們來(lái)看一下程序結(jié)構(gòu),如圖5所示
(圖5)
整個(gè)Solution最主要由3個(gè)Project組成。
1. DataAccessLayer.SQLite包裝了對(duì)SQLite訪問(wèn)的方法
2. ForSingle 主程序
3. UserControls 自定義用戶控件
需要說(shuō)明的是:
這個(gè)工具所有界面最下方的3個(gè)按鈕保持統(tǒng)一,所以我在UserControls中畫(huà)了一個(gè)BaseForm(圖中橙色框標(biāo)出),讓主界面繼承自BaseForm。
其他的每一個(gè)界面都做成UserControl,在主程序中控制它們的創(chuàng)建與顯示。如圖中綠色框標(biāo)出。
SQLite對(duì)于本地應(yīng)用是個(gè)不錯(cuò)的選擇,我使用的是一個(gè)包裝成ADO.NET接口的SQLite引擎。以下鏈接供參考:
我使用的類(lèi)庫(kù):http://sqlite.phxsoftware.com/
SQLite官方網(wǎng)站:http://www.sqlite.org/
#p#
代碼分析
1. 程序啟動(dòng)
當(dāng)程序啟動(dòng)時(shí),需要做一下檢查和初始化工作。我把這些工作都放在啟動(dòng)框中完成。
Program.cs:
- [STAThread]
- static void Main()
- {
- Application.EnableVisualStyles();
- Application.SetCompatibleTextRenderingDefault(false);
- if (Splash.Instance.ShowDialog() == DialogResult.OK)
- {
- Application.Run(new MainFrame());
- }
- }
以上代碼中的Splash就是啟動(dòng)對(duì)話框。只有當(dāng)返回DialogResult.OK時(shí),才會(huì)啟動(dòng)主程序。
Splash對(duì)話框是一個(gè)簡(jiǎn)單單例模式的實(shí)現(xiàn)。
Splash.cs:
- private static Splash _instance;
- public static Splash Instance
- {
- get
- {
- if (_instance == null)
- {
- _instance = new Splash();
- }
- return _instance;
- }
- }
在Splash的構(gòu)造過(guò)程中,會(huì)啟動(dòng)一個(gè)定時(shí)器,再會(huì)啟動(dòng)一個(gè)工作線程運(yùn)行初始化程序。
Splash.cs:
- private Splash()
- {
- InitializeComponent();
- SetDialogInfo();
- Ticker.Start();
- Worker.RunWorkerAsync();
- }
工作線程與定時(shí)器之間由標(biāo)志DBState聯(lián)系起來(lái)的,工作線程置標(biāo)志,定時(shí)器輪詢標(biāo)志。
Splash.cs:
- private Timer _ticker;
- public Timer Ticker
- {
- get
- {
- if (_ticker == null)
- {
- _ticker = new Timer(this.components);
- _ticker.Interval = 2000;
- _ticker.Tick += new System.EventHandler(_ticker_Tick);
- }
- return _ticker;
- }
- }
- private enum DBStateEnum
- {
- Undefined,
- Ready,
- Failed
- }
- private DBStateEnum _dbState = DBStateEnum.Undefined;
- private DBStateEnum DBState
- {
- get { return _dbState; }
- set { _dbState = value; }
- }
- private void _ticker_Tick(object sender, System.EventArgs e)
- {
- if (DBState == DBStateEnum.Ready)
- {
- this.DialogResult = DialogResult.OK;
- this.Close();
- }
- else if (DBState == DBStateEnum.Failed)
- {
- if (string.IsNullOrEmpty(this.lblMessage.Text))
- {
- this.lblMessage.Text = ErrorMessage;
- }
- else
- {
- this.DialogResult = DialogResult.Cancel;
- this.Close();
- }
- }
- }
2. 標(biāo)簽選擇框的繪制
圖3下半部分中有一系列動(dòng)態(tài)標(biāo)簽,這些標(biāo)簽的顯示邏輯為:
從本地SQLite數(shù)據(jù)庫(kù)中,查詢出指定消費(fèi)類(lèi)別(‘生活必需’或‘奢侈享受’)近一個(gè)月中不重復(fù)的標(biāo)簽,按出現(xiàn)頻率倒序排列,并取出前10個(gè)
FeeRecorderControl.cs:
- private static readonly string getRecentMonthTop10SubCategorySql =
- @"select
- SubCategory
- from
- AccountRecord
- where
- Category = '{0}'
- and
- ConsumeDate >= date('now', 'localtime', '-1 month')
- and
- ConsumeDate <= datetime('now', 'localtime')
- and
- ifnull(SubCategory, '') <> ''
- group by
- SubCategory
- order by
- count(*) desc
- limit 0,10;";
界面上的繪制標(biāo)簽區(qū)域其實(shí)是一個(gè)Panel,每一個(gè)標(biāo)簽是一個(gè)Label。
每次添加Label時(shí),需檢查當(dāng)前將繪制的Label是否會(huì)超出Panel的邊界,并相應(yīng)的進(jìn)行換行處理或退出循環(huán)。
FeeRecorderControl.cs:
- private void InitalizeSubCategoryPanel(string strCategory, Color backColor)
- {
- using (SQLiteConnection conn = new SQLiteConnection(SqliteConnString))
- {
- conn.Open();
- using (SQLiteCommand cmd = new SQLiteCommand(string.Format(getRecentMonthTop10SubCategorySql, strCategory), conn))
- {
- using (SQLiteDataReader reader = cmd.ExecuteReader())
- {
- Point subCategoryLocation = new Point(0, 0);
- SubCategoryList.Clear();
- plSubCategory.Controls.Clear();
- while (reader.Read())
- {
- string strSubCategory = reader["SubCategory"].ToString();
- Label lblSubCategory = new Label();
- lblSubCategory.Text = strSubCategory;
- lblSubCategory.Font = new Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Bold,
- System.Drawing.GraphicsUnit.Point, ((byte) (0)));
- lblSubCategory.Width = lblSubCategory.Text.Length*25 + 10;
- lblSubCategory.Height = 35;
- lblSubCategory.TextAlign = ContentAlignment.MiddleCenter;
- lblSubCategory.BackColor = backColor;
- lblSubCategory.Click += new EventHandler(lblSubCategory_Click);
- if (subCategoryLocation.X + lblSubCategory.Width <= plSubCategory.Width
- && subCategoryLocation.Y + lblSubCategory.Height <= plSubCategory.Height)
- {
- lblSubCategory.Location = subCategoryLocation;
- }
- else if (subCategoryLocation.X + lblSubCategory.Width > plSubCategory.Width
- && subCategoryLocation.Y + lblSubCategory.Height + 5 + lblSubCategory.Height <= plSubCategory.Height)
- {
- subCategoryLocation.X = 0;
- subCategoryLocation.Y = subCategoryLocation.Y + lblSubCategory.Height + 5;
- lblSubCategory.Location = subCategoryLocation;
- }
- else
- {
- break;
- }
- subCategoryLocation.X = subCategoryLocation.X + lblSubCategory.Width + 5;
- SubCategoryList.Add(lblSubCategory);
- }
- plSubCategory.Controls.AddRange(SubCategoryList.ToArray());
- }
- }
- conn.Close();
- }
- }
總結(jié)與思考
1. 我對(duì)WinForm的開(kāi)發(fā)遠(yuǎn)沒(méi)有對(duì)數(shù)據(jù)庫(kù)開(kāi)發(fā)熟悉,大家若發(fā)現(xiàn)紕漏之處,請(qǐng)溫柔指出。
2. 最近用戶體驗(yàn)是一個(gè)熱門(mén)詞匯,做軟件除了考慮技術(shù)問(wèn)題之外,更要站在用戶的角度去考慮他們的使用習(xí)慣。
3. 我自己非常想把這個(gè)記賬工具做成手機(jī)版的,但對(duì)于移動(dòng)開(kāi)發(fā)知之甚少,大家可以進(jìn)行嘗試與討論,歡迎和我郵件交流。
原文鏈接:http://www.cnblogs.com/DBFocus/archive/2011/02/27/1966203.html
【編輯推薦】
- SQLite做為本地緩存應(yīng)注意的幾大方面
- C#中數(shù)據(jù)本地存儲(chǔ)方案之SQLite
- 淺析SQLite數(shù)據(jù)庫(kù)開(kāi)發(fā)常用管理工具
- Widget開(kāi)發(fā)心得 解決跳轉(zhuǎn)頁(yè)面和SQLite類(lèi)問(wèn)題