為自己做一個簡單記賬簿
每個月收到信用卡賬單時,我總會又驚又惑。上個月怎么又花了那么多錢?看著每一筆出帳流水,猛抓頭皮卻怎么也記不起來這錢是用在了哪兒。痛定思痛,采取行動,我要記賬。作為一個信奉技術能改變世界的IT人,我理所當然的在網(wǎng)上搜索各種電子記賬本。在線的記賬功能不敢用(怕被騷擾),一些單機記賬軟件提供的功能又不是我想要的。
與此同時,最近空下來的時候,我在看SQLite方面的資料。SQLite的簡潔、小巧讓我有些愛不釋手。就此決定給自己做個記賬本,用SQLite作為本地數(shù)據(jù)引擎。
功能概述
我需要的記賬功能比較簡單:
***、記錄每一筆消費,并可以添加需要的標簽。當我查看明細時,能知道自己買了啥。
第二、對我來說,消費只需要分成兩種:‘生活必需消費’和‘享受消費’。每周、每月可以看到這兩種消費所占的比例、金額。
第三、能查看自己近6個月的消費走勢。
根據(jù)這3點需求,我為自己度身定制了這款記賬工具。
圖1是記賬本的啟動框。
程序?qū)右粋€工作線程來檢查記賬程序路徑下是否已存在賬本數(shù)據(jù)庫,若沒有則創(chuàng)建該數(shù)據(jù)庫和所需的表結(jié)構。同時定時器將輪詢檢查結(jié)果。
(圖1)
圖2是記賬本的主界面。
很多其他記賬軟件把消費分成餐飲,交通,買衣服……或者更細。一筆賬到底歸為哪一類要想個半天,同時出的圖表復雜但又意義不大。
為自己做的賬本只有兩種消費類別,對應兩個大按鈕,點擊即可進入記賬界面。這兩種消費所占的比例和總額是我每月的關注點。
主界面的最下方還有3個按鈕,分別對應‘返回主界面’、‘退出程序’、‘查看報表’。在任何其它界面中,這三個按鈕的圖案、功能都保持一致。
(圖2)
點擊主界面上的綠色或紅色按鈕就會進入到記賬界面。如圖3所示
標題、圖標、主色調(diào)區(qū)分了不同的消費。該界面的設計也是希望最簡化,省去了消費時間選擇框,默認為當前記錄時間。
該界面的一個亮點是‘標簽選擇框’??蛑械臉撕炇莿討B(tài)生成的。系統(tǒng)會取近一個月時間,使用最頻繁的10個標簽來顯示。(代碼分析部分還會展開)
這里記錄的標簽,會出現(xiàn)在后面的明細報表中,這是我用來對賬的。
(圖3)
***來看一下這個小工具能生成的圖表與報表,如圖4所示
該工具能輸出3種報表,分別是消費比例圖,近6月消費走勢圖,消費對賬明細。對于圖表,鼠標至于色塊上方時將顯示消費金額。
這3個報表也本著減少操作,降低復雜度,簡潔好用為宗旨,所以只提供了最必要的功能。
(圖4)
程序結(jié)構
看了工具的界面設計后,讓我們來看一下程序結(jié)構,如圖5所示
(圖5)
整個Solution最主要由3個Project組成。
1. DataAccessLayer.SQLite包裝了對SQLite訪問的方法
2. ForSingle 主程序
3. UserControls 自定義用戶控件
需要說明的是:
這個工具所有界面最下方的3個按鈕保持統(tǒng)一,所以我在UserControls中畫了一個BaseForm(圖中橙色框標出),讓主界面繼承自BaseForm。
其他的每一個界面都做成UserControl,在主程序中控制它們的創(chuàng)建與顯示。如圖中綠色框標出。
SQLite對于本地應用是個不錯的選擇,我使用的是一個包裝成ADO.NET接口的SQLite引擎。以下鏈接供參考:
我使用的類庫:http://sqlite.phxsoftware.com/
SQLite官方網(wǎng)站:http://www.sqlite.org/
#p#
代碼分析
1. 程序啟動
當程序啟動時,需要做一下檢查和初始化工作。我把這些工作都放在啟動框中完成。
Program.cs:
- [STAThread]
- static void Main()
- {
- Application.EnableVisualStyles();
- Application.SetCompatibleTextRenderingDefault(false);
- if (Splash.Instance.ShowDialog() == DialogResult.OK)
- {
- Application.Run(new MainFrame());
- }
- }
以上代碼中的Splash就是啟動對話框。只有當返回DialogResult.OK時,才會啟動主程序。
Splash對話框是一個簡單單例模式的實現(xiàn)。
Splash.cs:
- private static Splash _instance;
- public static Splash Instance
- {
- get
- {
- if (_instance == null)
- {
- _instance = new Splash();
- }
- return _instance;
- }
- }
在Splash的構造過程中,會啟動一個定時器,再會啟動一個工作線程運行初始化程序。
Splash.cs:
- private Splash()
- {
- InitializeComponent();
- SetDialogInfo();
- Ticker.Start();
- Worker.RunWorkerAsync();
- }
工作線程與定時器之間由標志DBState聯(lián)系起來的,工作線程置標志,定時器輪詢標志。
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. 標簽選擇框的繪制
圖3下半部分中有一系列動態(tài)標簽,這些標簽的顯示邏輯為:
從本地SQLite數(shù)據(jù)庫中,查詢出指定消費類別(‘生活必需’或‘奢侈享受’)近一個月中不重復的標簽,按出現(xiàn)頻率倒序排列,并取出前10個
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;";
界面上的繪制標簽區(qū)域其實是一個Panel,每一個標簽是一個Label。
每次添加Label時,需檢查當前將繪制的Label是否會超出Panel的邊界,并相應的進行換行處理或退出循環(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. 我對WinForm的開發(fā)遠沒有對數(shù)據(jù)庫開發(fā)熟悉,大家若發(fā)現(xiàn)紕漏之處,請溫柔指出。
2. 最近用戶體驗是一個熱門詞匯,做軟件除了考慮技術問題之外,更要站在用戶的角度去考慮他們的使用習慣。
3. 我自己非常想把這個記賬工具做成手機版的,但對于移動開發(fā)知之甚少,大家可以進行嘗試與討論,歡迎和我郵件交流。
原文鏈接:http://www.cnblogs.com/DBFocus/archive/2011/02/27/1966203.html
【編輯推薦】
- SQLite做為本地緩存應注意的幾大方面
- C#中數(shù)據(jù)本地存儲方案之SQLite
- 淺析SQLite數(shù)據(jù)庫開發(fā)常用管理工具
- Widget開發(fā)心得 解決跳轉(zhuǎn)頁面和SQLite類問題