鋼琴練習,只需鍵盤即可,但添加音階(scale),和弦(chord),和弦進行(chord progression)的處理,五線譜(score)的顯示,似乎也還不錯,本以為 tkinter 的界面編程偏弱,但通過學習,發現其快速強大,絲毫不弱!
1. 效果圖

2. 代碼
import os import sys import tkinter as tk from tkinter import colorchooser, filedialog, messagebox # 為參考 utils,在 site-packages 目錄下新建 mypath.pth 檔案, # 添加所需匯入模塊的目錄路徑, 如 ‘x01.lab/py/’ 所在路徑, import utils sys.path.append(utils.R.CurrentDir) from piano.core import PianoFrame, R class MainWindow(tk.Tk): Title = 'x01.piano' def __init__(self): super().__init__() self.title(self.Title) utils.R.win_center(self,w=R.WinWidth, h=R.WinHeight) self.resizable(False, False) self.menu = tk.Menu(self) utils.R.generate_menus(self,['file', 'help']) self.configure(menu=self.menu) self.piano_frame = PianoFrame(self, width=R.WinWidth, height=R.WinHeight) self.piano_frame.pack() def file_quit(self): self.destroy() if __name__ == "__main__": win = MainWindow() win.mainloop()main.py
import json import os import sys import time import tkinter as tk import tkinter.ttk as ttk from collections import OrderedDict from tkinter import colorchooser, filedialog, messagebox import itertools from functools import partial import simpleaudio import utils from _thread import start_new_thread class R: WinWidth = 560 ModeHeight = 50 ScoreHeight = 110 ControlHeight = 100 KeyHeight = 160 WinHeight = ModeHeight + ControlHeight + KeyHeight + ScoreHeight Choices = ['Scales', 'Chords', 'Chord Progressions'] ImagePath = os.path.join(utils.R.CurrentDir, 'piano/img/') SoundsPath = os.path.join(utils.R.CurrentDir, 'piano/sounds/') JsonPath = os.path.join(utils.R.CurrentDir, 'piano/json/') WhiteKeyNames = ['C1','D1', 'E1', 'F1', 'G1','A1', 'B1', 'C2','D2', 'E2', 'F2', 'G2','A2', 'B2'] BlackKeyNames = ['C#1', 'D#1', 'F#1', 'G#1', 'A#1', 'C#2', 'D#2', 'F#2', 'G#2', 'A#2'] WhiteKeyXCoordinates = [0,40, 80,120, 160, 200, 240,280, 320, 360, 400, 440, 480,520] BlackKeyXCoordinates = [30,70,150,190, 230, 310, 350, 430,470, 510] AllKeys= ['C1','C#1','D1','D#1','E1','F1','F#1','G1','G#1','A1', 'A#1','B1', 'C2','C#2','D2','D#2','E2','F2','F#2','G2', 'G#2','A2','A#2','B2'] Keys = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'] Romans = { 'I':0, 'II': 2, 'III':4, 'IV':5, 'V': 7, 'VI':9, 'VII': 11, 'i':0, 'ii': 2, 'iii':4, 'iv':5, 'v': 7, 'vi':9, 'vii': 11} class PianoFrame(tk.Frame): def __init__(self, master=None, cnf=None, **kw): super().__init__(master=master, cnf=cnf, **kw) self.master = master self.images = { 'black_key': tk.PhotoImage(file=os.path.join(R.ImagePath, 'black_key.gif')), 'white_key': tk.PhotoImage(file=os.path.join(R.ImagePath, 'white_key.gif')), 'black_key_pressed': tk.PhotoImage(file=os.path.join(R.ImagePath, 'black_key_pressed.gif')), 'white_key_pressed': tk.PhotoImage(file=os.path.join(R.ImagePath, 'white_key_pressed.gif')) } self.player = AudioPlayer() self.keys = [] self.highlight_keys = [] self.progression_buttons = [] self.scales = self.load_json_files(filename=os.path.join(R.JsonPath, 'scales.json')) self.chords = self.load_json_files(filename=os.path.join(R.JsonPath, 'chords.json')) self.progressions = self.load_json_files(filename=os.path.join(R.JsonPath, 'progressions.json')) self.create_mode_frame() self.create_score_frame() self.create_control_frame() self.create_key_frame() self.create_chords_frame() self.create_progression_frame() self.create_scales_frame() self.find_scale() def create_mode_frame(self): frame = tk.Frame(self, width=R.WinWidth, height=R.ModeHeight) frame.grid_propagate(False) mode_combox = ttk.Combobox(frame, values=R.Choices) mode_combox.bind('<<ComboboxSelected>>', self.mode_selected) mode_combox.current(0) mode_combox.grid() frame.grid(row=0,column=0) self.mode_combox = mode_combox def mode_selected(self, e=None): mode = self.mode_combox.get() if mode == 'Scales': self.show_scales_frame() elif mode == 'Chords': self.show_chords_frame() elif mode == 'Chord Progressions': self.show_progression_frame() def show_scales_frame(self): self.chords_frame.grid_remove() self.progression_frame.grid_remove() self.scales_frame.grid() def show_chords_frame(self): self.scales_frame.grid_remove() self.progression_frame.grid_remove() self.chords_frame.grid() def show_progression_frame(self): self.scales_frame.grid_remove() self.chords_frame.grid_remove() self.progression_frame.grid() def create_score_frame(self): frame = tk.Frame(self, width=R.WinWidth, height=R.ScoreHeight) frame.grid_propagate(False) frame.grid(row=1,column=0) self.score_frame = frame self.score_maker = ScoreMaker(self.score_frame) def create_control_frame(self): frame = tk.Frame(self, width=R.WinWidth, height=R.ControlHeight) frame.grid_propagate(False) frame.grid(row=2,column=0) self.control_frame = frame def create_key_frame(self): frame = tk.Frame(self, width=R.WinWidth, height=R.KeyHeight, background='LavenderBlush2') frame.grid_propagate(False) tk.Label(frame, text='placeholder for key frame').grid() frame.grid(row=4,column=0, sticky='nsew') self.key_frame = frame for i, key in enumerate(R.WhiteKeyNames): x = R.WhiteKeyXCoordinates[i] self.create_key(self.images['white_key'], key, x) for i, key in enumerate(R.BlackKeyNames): x = R.BlackKeyXCoordinates[i] self.create_key(self.images['black_key'], key, x) def create_key(self, image, key, x): label = tk.Label(self.key_frame, image=image, border=0) label.place(x=x, y=0) label.name = key label.bind('<Button-1>', self.key_pressed) label.bind('<ButtonRelease-1>', self.key_released) self.keys.append(label) return label def key_pressed(self, e=None): self.player.play_note(e.widget.name) if len(e.widget.name) == 2: img = self.images['white_key_pressed'] elif len(e.widget.name) == 3: img = self.images['black_key_pressed'] e.widget.config(image=img) def key_released(self, e=None): if len(e.widget.name) == 2: img = self.images['white_key'] elif len(e.widget.name) == 3: img = self.images['black_key'] e.widget.config(image=img) def create_scales_frame(self): frame = tk.Frame(self.control_frame, width=R.WinWidth, height=R.ControlHeight) tk.Label(frame, text='Select scale').grid(row=0,column=1,stick='w',padx=10,pady=1) scale_combox = ttk.Combobox(frame, values=[k for k in self.scales.keys()]) scale_combox.current(0) scale_combox.bind('<<ComboboxSelected>>', self.scale_changed) scale_combox.grid(row=1, column=1, sticky='e', padx=10, pady=10) tk.Label(frame, text='In the key of').grid(row=0, column=2, sticky='w', padx=10, pady=1) scale_key_combox = ttk.Combobox(frame, values=[k for k in R.Keys]) scale_key_combox.current(0) scale_key_combox.bind('<<ComboboxSelected>>', self.scale_key_changed) scale_key_combox.grid(row=1, column=2, sticky='e', padx=10, pady=10) frame.grid(row=1,column=0, sticky='nsew') self.scales_frame = frame self.scale_combox = scale_combox self.scale_key_combox = scale_key_combox def scale_changed(self, e=None): self.remove_all_key_highlight() self.find_scale(e) def scale_key_changed(self, e=None): self.remove_all_key_highlight() self.find_scale(e) def remove_all_key_highlight(self): for key in self.highlight_keys: self.remove_key_highlight(key) self.highlight_keys = [] def remove_key_highlight(self, key): if len(key) == 2: img = self.images['white_key'] elif len(key) == 3: img = self.images['black_key'] for w in self.keys: if w.name == key: w.configure(image=img) def find_scale(self, e=None): self.selected_scale = self.scale_combox.get() self.scale_selected_key = self.scale_key_combox.get() index = R.Keys.index(self.scale_selected_key) self.highlight_keys = [R.AllKeys[i+index] for i in self.scales[self.selected_scale]] self.highlight_list_of_keys(self.highlight_keys) self.player.play_scale_in_new_thread(self.highlight_keys) self.score_maker.draw_notes(self.highlight_keys) def highlight_list_of_keys(self, key_names): for key in key_names: self.highlight_key(key) def highlight_key(self, key): if len(key) == 2: img = self.images['white_key_pressed'] elif len(key) == 3: img = self.images['black_key_pressed'] for w in self.keys: if w.name == key: w.configure(image=img) def create_chords_frame(self): frame = tk.Frame(self.control_frame, width=R.WinWidth, height=R.ControlHeight) frame.grid_propagate(False) frame.grid(row=1,column=0, sticky='nsew') tk.Label(frame, text='Selected Chord').grid(row=0,column=1, sticky='w', padx=10, pady=1) chords_combox = ttk.Combobox(frame, values=[k for k in self.chords.keys()]) chords_combox.current(0) chords_combox.bind('<<ComboboxSelected>>', self.chord_changed) chords_combox.grid(row=1, column=1, sticky='e', padx=10, pady=10) tk.Label(frame, text='in the key of').grid(row=0, column=2, sticky='w', padx=10, pady=1) chords_key_combox = ttk.Combobox(frame, values=[k for k in R.Keys]) chords_key_combox.current(0) chords_key_combox.bind('<<ComboboxSelected>>', self.chords_key_changed) chords_key_combox.grid(row=1, column=2, sticky='e', padx=10, pady=10) self.chords_combox = chords_combox self.chords_key_combox = chords_key_combox self.chords_frame = frame def chord_changed(self, e=None): self.remove_all_key_highlight() self.find_chord(e) def chords_key_changed(self, e=None): self.remove_all_key_highlight() self.find_chord(e) def find_chord(self, e=None): self.selected_chord = self.chords_combox.get() self.chords_selected_key = self.chords_key_combox.get() index = R.Keys.index(self.chords_selected_key) self.highlight_keys = [R.AllKeys[i+index] for i in self.chords[self.selected_chord]] self.score_maker.draw_chord(self.highlight_keys) self.highlight_list_of_keys(self.highlight_keys) self.player.play_chord_in_new_thread(self.highlight_keys) def create_progression_frame(self): frame = tk.Frame(self.control_frame, width=R.WinWidth, height=R.ControlHeight) frame.grid_propagate(False) frame.grid(row=1,column=0, sticky='nsew') tk.Label(frame, text='Select Scales').grid(row=0, column=1, sticky='w', padx=10, pady=1) tk.Label(frame, text='Select Progression').grid(row=0,column=2,sticky='w', padx=10, pady=1) tk.Label(frame, text='in the key of').grid(row=0, column=3, sticky='w', padx=10, pady=1) progression_scale_combox = ttk.Combobox(frame, values=[k for k in self.progressions.keys()], width=18) progression_scale_combox.bind('<<ComboboxSelected>>', self.progression_scale_changed) progression_scale_combox.current(0) progression_scale_combox.grid(row=1, column=1, sticky='w', padx=10, pady=5) progression_combbox = ttk.Combobox(frame, values=[k for k in self.progressions['Major'].keys()], width=18) progression_combbox.bind('<<ComboboxSelected>>', self.progression_changed) progression_combbox.current(0) progression_combbox.grid(row=1, column=2, sticky='w', padx=10, pady=5) progression_key_combox = ttk.Combobox(frame, values=R.Keys, width=18) progression_key_combox.current(0) progression_key_combox.bind('<<ComboboxSelected>>', self.progression_key_changed) progression_key_combox.grid(row=1, column=3, sticky='w', padx=10, pady=5) self.progression_combbox = progression_combbox self.progression_key_combox = progression_key_combox self.progression_scale_combox = progression_scale_combox self.progression_frame = frame def progression_changed(self, e=None): self.show_progression_buttons() def progression_key_changed(self, e=None): self.show_progression_buttons() def progression_scale_changed(self, e=None): selected_progression_scale = self.progression_scale_combox.get() progressions = [k for k in self.progressions[selected_progression_scale].keys()] self.progression_combbox['values'] = progressions self.progression_combbox.current(0) self.show_progression_buttons() def show_progression_buttons(self): self.destory_current_progression_buttons() selected_progression_scale = self.progression_scale_combox.get() selected_progression = self.progression_combbox.get().split('-') self.progression_buttons = [] for i in range(len(selected_progression)): self.progression_buttons.append(tk.Button(self.progression_frame, text=selected_progression[i], command=partial(self.progression_button_clicked, i))) sticky = ('w' if i == 0 else 'e') col = (i if i>1 else 1) self.progression_buttons[i].grid(row=2, column=col, sticky=sticky, padx=5) def progression_button_clicked(self, i): self.remove_all_key_highlight() selected_progression = self.progression_combbox.get().split('-')[i] if any(x.isupper() for x in selected_progression): selected_chord = 'Major' else: selected_chord = 'Minor' key_offset = R.Romans[selected_progression] selected_key = self.progression_key_combox.get() index = (R.Keys.index(selected_key) + key_offset) % 12 self.highlight_keys = [R.AllKeys[j + index] for j in self.chords[selected_chord]] self.score_maker.draw_chord(self.highlight_keys) self.highlight_list_of_keys(self.highlight_keys) self.player.play_chord_in_new_thread(self.highlight_keys) def destory_current_progression_buttons(self): for b in self.progression_buttons: b.destroy() def load_json_files(self, filename): with open(filename, 'r') as f: data = json.load(f, object_pairs_hook=OrderedDict) return data class AudioPlayer: def play_note(self, note_name): wave = simpleaudio.WaveObject.from_wave_file(os.path.join(R.SoundsPath, note_name + '.wav')) wave.play() def play_scale(self, scale): for note in scale: self.play_note(note) time.sleep(0.5) def play_scale_in_new_thread(self, scale): start_new_thread(self.play_scale, (scale, )) def play_chord(self, chord): for note in chord: self.play_note(note) def play_chord_in_new_thread(self, chord): start_new_thread(self.play_chord, (chord, )) class ScoreMaker(tk.Frame): def __init__(self, master=None): super().__init__(master=master) self.canvas = tk.Canvas(self.master, width=500, height=R.ScoreHeight) self.canvas.grid(row=0, column=1) self.master = master master.update_idletasks() self.canvas_width = R.WinWidth self.sharp_image = tk.PhotoImage(file=os.path.join(R.ImagePath, 'sharp.gif')) self.treble_clef_image = tk.PhotoImage(file=os.path.join(R.ImagePath, 'treble_clef.gif')) self.x_counter = itertools.count(start=50, step=30) def _clean_score_sheet(self): self.x_counter = itertools.count(start=50, step=30) self.canvas.delete('all') def _create_treble_staff(self): self._draw_five_lines() self.canvas.create_image(10,20,image=self.treble_clef_image, anchor='nw') def draw_chord(self, chord): self._clean_score_sheet() self._create_treble_staff() for note in chord: self._draw_single_note(note, is_in_chord=True) def _draw_five_lines(self): w = self.canvas_width for i in range(5): self.canvas.create_line(0,40+i*10, w, 40+i*10, fill='#555') def draw_notes(self, notes): self._clean_score_sheet() self._create_treble_staff() for note in notes: self._draw_single_note(note) def _draw_single_note(self, note, is_in_chord=False): is_sharp = '#' in note note = note.replace('#', '') radius = 9 if is_in_chord: x = 75 else: x = next(self.x_counter) i = R.WhiteKeyNames.index(note) y = 85 - 5*i self.canvas.create_oval(x,y,x+radius, y+radius, fill='#555') if is_sharp: self.canvas.create_image(x-10, y, image=self.sharp_image, anchor='nw') if note == 'C1': self.canvas.create_line(x-5, 90, x+15, 90, fill='#555') elif note == 'G2': self.canvas.create_line(x-5, 35, x+15, 35, fill='#555') elif note == 'A2': self.canvas.create_line(x-5, 35, x+15, 35, fill='#555') elif note == 'B2': self.canvas.create_line(x-5, 35, x+15, 35, fill='#555') self.canvas.create_line(x-5, 25, x+15, 25, fill='#555')core.py
3. 下載
x01.lab/py/piano
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/141430.html
標籤:Python
上一篇:pyecharts使用:TooltipOpts的使用及引數配置(目的解決提示框浮層和 axisPointer同時顯示提示框問題)
