python用的時間不長,,一般用來做字符串處理,、簡單測試的一些小程序。最近工作中需要做一個簡單的GUI應(yīng)用,,使用麥克錄音并存成wave文件,。然后就想拿wxPython練練手。
一,、概述
GUI開發(fā)采用wxPython,,界面編輯工具采用wxGlade,聲音采集和播放采用PyAudio,,小數(shù)據(jù)庫采用sqlite3,,最后使用py2exe打包發(fā)布?;镜膽?yīng)用開發(fā)流程都包括了,。
二、wxGlade
界面編輯工具也是找了幾個,,比如wxFormBuilder,,甚至是收費的DialogBlocks,wxFormBuilder 很漂亮,,不過bug好像較多,,經(jīng)常自動退出。wxGlade有經(jīng)典的Linux GUI界面風(fēng)格,,分立式窗體,,了解了基本的原理后用起來很方便。主要是其中的sizer,,add slot,、insert slot增加空位,,然后添加控件。也可以添加自定義的控件,,只需要設(shè)置自定義控件的Class屬性,。然后在MainFrame的Extra code for this widget增加from YourModule import YourCLass。因為界面比較簡單,,我沒有采用XRC資源導(dǎo)入的模式,,而是直接生成MainFrame的代碼。由于界面設(shè)計可能會變,,在應(yīng)用中新建一 個MainFrameEx類繼承MainFrame,,將事件處理放在繼承類中完成。這樣每次使用wxGlade編輯界面后可以直接覆蓋生成的代碼,。
三,、PyAudio
PyAudio是從PortAudio移植的,現(xiàn)在還是alpha版,。不過使用起來還真是方便,,看看網(wǎng)站上提供的example就可以了。沒有什么大問題,。需要注意多線程的問題,,PyAudio對象盡量復(fù)用。注意線程中刷新wxWidget需要使用wx.CallAfter方法,。
# -*- coding: UTF-8 -*- import pyaudio import wave import threading import wx import datetime import traceback import os import logging logger = logging.getLogger("root") class BMRecord(threading.Thread): CHUNK = 1024 FORMAT = pyaudio.paInt16 # 至少為16位 DEVICE = 1 CHANNELS = 1 RATE = 44100 RECORD_SECONDS = 5 def __init__(self, window, audio, device, prefix): threading.Thread.__init__(self) self.window = window self.audio = audio self.prefix = prefix # wave文件命名的前綴 self.DEVICE = device self.CHANNELS = 2 # 雙通道采集 self.RECORD_SECONDS = int(window.params["length"]) self.RATE = int(window.params["rate"]) self.FORMAT = pyaudio.paInt16 def record(self): self.filename = "" self.filetime = "" try : stream = self.audio.open(format=self.FORMAT, channels=self.CHANNELS, rate=self.RATE, input=True, input_device_index=self.DEVICE, frames_per_buffer=self.CHUNK) frames = [] for i in range(0, int(self.RATE * self.RECORD_SECONDS / self.CHUNK)): data = stream.read(self.CHUNK) frames.append(data) i = i stream.stop_stream() stream.close() now = datetime.datetime.now() strnow = now.strftime('%Y%m%d%H%M%S') self.filetime = now.strftime('%Y-%m-%d %H:%M:%S') savepath = self.checkPath(strnow) # 雙聲道存為兩個單聲道文件 frames1 = [] frames2 = [] wavedata = b''.join(frames) for i in range(len(wavedata) / 4): frames1.append(wavedata[i * 4 : i * 4 + 2]) frames2.append(wavedata[i * 4 + 2 : i * 4 + 4]) # 雙聲道存儲 fullpath = savepath + "/" + strnow + self.prefix + "X.wav" wf = wave.open(fullpath, 'wb') wf.setnchannels(2) wf.setsampwidth(self.audio.get_sample_size(self.FORMAT)) wf.setframerate(self.RATE) wf.writeframes(wavedata) wf.close() # 兩個單聲道存儲 filenames = [strnow + self.prefix + "Z.wav", strnow + self.prefix + "Y.wav"] fullpath = savepath + "/" + filenames[0] wf = wave.open(fullpath, 'wb') wf.setnchannels(1) wf.setsampwidth(self.audio.get_sample_size(self.FORMAT)) wf.setframerate(self.RATE) wf.writeframes(b''.join(frames1)) wf.close() fullpath = savepath + "/" + filenames[1] wf = wave.open(fullpath, 'wb') wf.setnchannels(1) wf.setsampwidth(self.audio.get_sample_size(self.FORMAT)) wf.setframerate(self.RATE) wf.writeframes(b''.join(frames2)) wf.close() self.message = "錄制成功" self.filenames = filenames logger.info(filenames[0] + "," + filenames[1] + ", recorded") return 0 except Exception: self.message = traceback.format_exc() logger.error(traceback.format_exc()) return -1 def checkPath(self, pathname): curpath = os.path.abspath(os.curdir) strdate = pathname[0:8] fullpath = curpath + "/data/" + strdate if not os.path.exists(fullpath) : os.makedirs(fullpath) return fullpath def run(self): ret = self.record() wx.CallAfter(self.window.recordResult, ret, self.filenames, self.filetime, self.message)
四,、sqlite
sqlite模塊是Python內(nèi)置的用起來很方便:
import sqlite3 import datetime class BMDatabase(): def loadData(self, whichDay): conn = sqlite3.connect("data/bmon.db") cur = conn.cursor() start = datetime.datetime.strptime(whichDay, "%Y-%m-%d") end = start + datetime.timedelta(days=1) res = cur.execute("select * from CheckRecord where rec_time between ? and ? order by rec_time", (start, end)) return res.fetchall() def getFile(self, waveFile): conn = sqlite3.connect("data/bmon.db") cur = conn.cursor() res = cur.execute("select * from CheckRecord where rec_file=?", (waveFile,)) row = res.fetchone() return row def save(self, filename, filetime, result): conn = sqlite3.connect("data/bmon.db") cur = conn.cursor() record = [(filename, filetime, result)] cur.executemany('INSERT INTO CheckRecord (rec_file,rec_time,result) VALUES (?,?,?)', record) conn.commit()
五、自定義wxWidget控件
自繪控件主要是處理EVT_PAINT事件: self.Bind(wx.EVT_PAINT, self.on_paint)
import wx class WavePane(wx.StaticText): waveData = None spectrum = None def __init__(self, parent, nid=wx.ID_ANY, caption=""): wx.StaticText.__init__(self, parent) self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) self.Bind(wx.EVT_SIZE, self.on_size) self.Bind(wx.EVT_PAINT, self.on_paint) def on_size(self, event): self.Refresh() event.Skip() def on_paint(self, event): w, h = self.GetClientSize() dc = wx.AutoBufferedPaintDC(self) brush = wx.Brush(wx.Color(0, 0, 0x80), wx.SOLID) dc.SetBrush(brush) dc.Clear() dc.SetPen(wx.Pen(wx.BLACK, 1)) dc.SetTextForeground(wx.Color(0, 0xFF, 0)) font = dc.GetFont() font.SetPointSize(8) dc.SetFont(font) dc.DrawRectangle(0, 0, w - 1, h - 1) if self.waveData <> None: dc.BeginDrawing() dc.SetPen(wx.Pen(wx.Color(0, 0xFF, 0), 1)) au = self.waveData step = int(au.nframes / w) height = au.height # 或者65536 / 2.0 i = 0 j = 0 while i < au.nframes: if au.frames[2 * i + 1] >= 0x80: # 負(fù)數(shù) value = au.frames[2 * i] + au.frames[2 * i + 1] * 256 - 65536 else: value = au.frames[2 * i] + au.frames[2 * i + 1] * 256 dc.DrawLine(j, int(h / 2.0), j, int(h / 2.0 * (1 - value * 1.0 / height))) i += step j += 1 if 2 * i + 1 >= au.nframes * 2: break; dc.DrawText(str(au.maxValue), 1, 1) dc.DrawText(str(au.minValue), 1, h - 16) dc.EndDrawing() elif self.spectrum <> None : dc.BeginDrawing() dc.SetPen(wx.Pen(wx.Color(0, 0xFF, 0), 1)) brush = wx.Brush(wx.Color(0, 0xFF, 0), wx.SOLID) dc.SetBrush(brush) dc.SetTextForeground(wx.RED) barWidth = int(w / 72) i = 0 j = barWidth while i < 36: y = int((1 - self.spectrum[i] * 1.0 / self.maxSpectrum) * h) dc.DrawRectangle(j, y , barWidth, h - y - 1) if y < h - 2: dc.FloodFill(j + 1, y + 1, wx.Color(0, 0xFF, 0), wx.FLOOD_BORDER) # Bar的編號 if i == 0 or(i + 1) % 5 == 0: if i < 9: dc.DrawText(str(i + 1), j + 4, h - 13) else: dc.DrawText(str(i + 1), j , h - 13) i += 1 j += barWidth * 2 dc.SetTextForeground(wx.Color(0, 0xFF, 0)) dc.DrawText(str(self.maxSpectrum), 1, 1) dc.EndDrawing() def setWaveData(self, waveData): self.waveData = waveData self.Refresh() def setSpectrum(self, spectrum): self.spectrum = spectrum if spectrum <> None: maxValue = 0 for i in range(0, 36): if spectrum[i] > maxValue : maxValue = spectrum[i] self.maxSpectrum = maxValue self.Refresh()
六,、日志的使用
使用循環(huán)日志
import logging.handlers logger = logging.getLogger("root") handler = logging.handlers.RotatingFileHandler(os.path.join(os.getcwd(), 'bmon.log'), maxBytes=5 * 1024 * 1024, backupCount=5) formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(logging.DEBUG)
七,、py2exe
最后一步就是打包成可執(zhí)行文件。setup.py:
from distutils.core import setup import py2exe # setup(console=["hello.py"]) py2exe_options = dict({ "includes":['sip', 'encodings', 'encodings.ascii', 'encodings.utf_8', 'encodings.cp866'], "dll_excludes":["MSVCP90.dll"]}) setup(version="1.0", description="Bearing Monitor", name="bmon", zipfile=None, dist_dir="bmon", windows=["bmon.py"], options={'py2exe': py2exe_options}, icon_resources=[(1, "check_all.ico")], data_files=[("", ["check_all.ico"])] )
然后命令行下執(zhí)行:python setup.py py2exe,,就可以生成dist發(fā)布目錄
八,、小結(jié)
這個簡單應(yīng)用涉及的主要模塊就這么幾個,組合成了一個簡單的GUI應(yīng)用,。Python開發(fā)還真是很簡單,,前提是得熟悉各種模塊。
|