class Application(tk.Frame): def __init__(self, master): # master is set to root(tk.TK()) at the bottom super().__init__(master) self.master = master self.js = JsonMaker() # init variables global isSelected isSelected = False # Widget Title master.title('RSS Feed Reader') # Width height master.geometry("1600x900") # change background color of root, color from https://html-color-codes.info/ master.configure(bg="#585858") # set title bar icon master.iconbitmap('Icons/RSS_Icon.ico') # Create widgets/grid self.create_widgets() # Init selected item var self.selected_item = 0 # Populate initial list self.populate_list() # region Functions # Create GUI def create_widgets(self): # Frames self.frm = tk.Frame(self.master, bg="#585858") self.frm.pack() # GUI elements self.spaceFrame = tk.Label(self.frm, text="RSS Entries Viewer", relief=GROOVE, anchor=CENTER, font='Times 12 italic', bg='#424242', fg='#FFFFFF') self.spaceFrame.grid(row=0, column=3, columnspan=9, pady=5, ipadx=10) self.spaceFrame = tk.Label(self.frm, text="Create Databases!", relief=GROOVE, anchor=CENTER, font='Times 12 italic', bg='#424242', fg='#FFFFFF') self.spaceFrame.grid(row=1, column=12, columnspan=3, pady=5, ipadx=10) self.spaceFrame = tk.Label(self.frm, text="Delete Files!", relief=GROOVE, anchor=CENTER, font='Times 12 italic', bg='#424242', fg='#FFFFFF') self.spaceFrame.grid(row=1, column=1, columnspan=1, pady=5, ipadx=10, padx=40) self.optionListbox = Listbox(self.frm, bg='#D8D8D8', height="25", width=150, border=0) self.optionListbox.grid(row=3, column=2, columnspan=10, rowspan=6, pady=5, padx=10) self.tv = ttk.Treeview(self.frm, columns=(1, 2, 3, 4), show="headings", height="20", style="Treeview") self.SummaryBox = Text(self.frm, height=10, width=100) self.SummaryBox.grid(row=10, column=2, columnspan=10, rowspan=6, pady=5, padx=20) self.enterString = Entry(self.frm, width=90) self.enterString.grid(row=1, column=4, columnspan=6, pady=5) self.clearInput = tk.Button(self.frm, text="Clear Text", command=self.clearTextbox) self.clearInput.grid(row=2, column=9, pady=15, padx=10) self.enterString_label = Label(self.frm, text="Enter RSS URL and Strings:", bg='#424242', fg='#FFFFFF') self.enterString_label.grid(row=1, column=3, pady=5, padx=10, ipadx=20) self.inputURL = tk.Button(self.frm, text="Enter RSS URL", command=self.enterURL) self.inputURL.grid(row=2, column=5, pady=15) self.cdbBtn = tk.Button(self.frm, text="Create DB", command=self.CreateDB) self.cdbBtn.grid(row=5, column=12, pady=5, padx=60) self.clearDBBtn = tk.Button(self.frm, text="Clear DB", command=self.ClearDB) self.clearDBBtn.grid(row=4, column=1, pady=5) self.clearGridBtn = tk.Button(self.frm, text="Table Name", command=self.getName) self.clearGridBtn.grid(row=2, column=3, pady=15) # need to expand the functionality to work with multiple tables in the same DB before using this function in the full build! # self.optionsunoBtn = tk.Button(self.frm, text="From Directory", command=self.findDirectory) # self.optionsunoBtn.grid(row=8, column=1, pady = 5) self.JSONBtn = tk.Button(self.frm, text="Make JSON", command=self.makeJson) self.JSONBtn.grid(row=3, column=12, pady=5) self.simpleBtn = tk.Button(self.frm, text="JSON Entries", command=self.makeJsonEntries) self.simpleBtn.grid(row=4, column=12, pady=5) self.clearGridBtn = tk.Button(self.frm, text="Clear Grid", command=self.clearGrid) self.clearGridBtn.grid(row=7, column=1, pady=5) # delete this button and all it's functions when the project is fully complete, this is for testing only! # self.clearGridBtn = tk.Button(self.frm, text="test vars", command=self.testVar) # self.clearGridBtn.grid(row=9, column=1, pady = 5) self.DBNameBtn = tk.Button(self.frm, text="DB Name", command=self.nameDB) self.DBNameBtn.grid(row=2, column=7, pady=15) self.EditDBBtn = tk.Button(self.frm, text="Edit DB", command=self.editDBWindow) self.EditDBBtn.grid(row=6, column=12, pady=5) self.clearJSONBtn = tk.Button(self.frm, text="Clear JSON", command=self.ClearJSON) self.clearJSONBtn.grid(row=5, column=1, pady=5) self.clearCSVBtn = tk.Button(self.frm, text="Clear CSV", command=self.ClearCSV) self.clearCSVBtn.grid(row=6, column=1, pady=5) self.copyLinkBtn = tk.Button(self.frm, text="Copy URL", command=self.copy_info) self.copyLinkBtn.grid(row=8, column=12, pady=5) self.ShowDBBtn = tk.Button(self.frm, text="Show DB", command=self.showDB) self.ShowDBBtn.grid(row=7, column=12, pady=5) self.removeEntryBtn = tk.Button(self.frm, text="Delete Entry!", command=self.deleteEntry) self.removeEntryBtn.grid(row=3, column=1, pady=5) self.spaceFrame = tk.Label(self.frm, text="Instructions:", relief=GROOVE, anchor=CENTER, font='Times 12 italic', bg='#424242', fg='#FFFFFF') self.spaceFrame.grid(row=18, column=3, columnspan=9, pady=5, ipadx=10) self.spaceFrame = tk.Label( self.frm, text= "Enter names for Database, Table, and the URL for RSS Feed using the first three buttons in the top row!", relief=GROOVE, anchor=CENTER, font='Times 12', bg='#424242', fg='#FFFFFF') self.spaceFrame.grid(row=19, column=3, columnspan=9, pady=2, ipadx=10) self.spaceFrame = tk.Label( self.frm, text= "After the names/URL are inserted, Click on the right side, then 'Make JSON', then 'JSON Entries', and then 'Create DB' in order to show the entries in the RSS Feed!", relief=GROOVE, anchor=CENTER, font='Times 12', bg='#424242', fg='#FFFFFF') self.spaceFrame.grid(row=20, column=3, columnspan=9, pady=2, ipadx=10) self.spaceFrame = tk.Label( self.frm, text= "Use 'Find Directory' button to get the main.JSON file from another table name inserted if you want to select another table, the rest of the buttons can be figured out by testing them manually!", relief=GROOVE, anchor=CENTER, font='Times 12', bg='#424242', fg='#FFFFFF') self.spaceFrame.grid(row=21, column=1, columnspan=17, pady=2, ipadx=10) # function to handle event that the user enters the RSS URL in textbox def enterURL(self): global entry if not self.enterString.get() == "": entry = self.enterString.get() self.clearTextbox() # function to get names from entry box def getName(self): global name if not self.enterString.get() == "": name = self.enterString.get() self.clearTextbox() # function to clear the textbox def clearTextbox(self): self.enterString.delete(0, END) # function to populate listbox with data from database if availible def populate_list(self): global DatabaseName global name if os.path.isdir('DB_Files/'): if not len(os.listdir('DB_Files/')) == 0: if not name == "" and not DatabaseName == "": self.showDB() else: DatabaseName, name = self.js.getDBandName() print(DatabaseName + " " + name) self.populate_list() else: pass else: pass # function to take in url and convert it to json def makeJson(self): self.js.makeWholeJSON(name, entry) # function to make json file for each entry in the feed def makeJsonEntries(self): self.js.getEntries() # function to set NewsFeed in Json.py to the main.JSON file located in directory specificed by user input def findDirectory(self): if not self.enterString.get() == "": indexValue = self.enterString.get() self.js.readFromJson(indexValue) self.clearTextbox() # function to get Database name from User def nameDB(self): global DatabaseName if not self.enterString.get() == "": DatabaseName = self.enterString.get() self.clearTextbox() # function to handle copying the URL of the entry from table the user wants def copy_info(self): if not currentLink == "": # save link from values to system clipboard pyperclip.copy(currentLink) # function to clear the JSON files from the directory def ClearJSON(self): self.js.wipeJSON() # function to clear the CSV files from the directory def ClearCSV(self): self.js.wipeCSV() # function to create database # function to create the database! def CreateDB(self): if not DatabaseName == "": if not name == "": table = name self.js.makeDB(DatabaseName, table) self.js.convertToSQL(DatabaseName, table) self.showDB() self.clearTextbox() else: print("Database creation failed!") else: print("Database name was null!") # function to clear the DB files from the directory def ClearDB(self): self.js.wipeDB() self.clearGrid() # function to clear the grid def clearGrid(self): self.tv.delete( *self.tv.get_children()) # delete saved treeview children self.tv.grid_remove() self.optionListbox.grid_remove() self.SummaryBox.delete("1.0", "end") self.optionListbox.grid(row=3, column=2, columnspan=10, rowspan=6, pady=5, padx=10) # function to display content from db def showDB(self): rows = [] self.clearGrid() # style the output graph, style name is "Treeview" as defined in style.configure self.style = ttk.Style() self.style.configure(".", font=('Helvetica', 8), foreground="white") self.style.configure("Treeview", foreground='red') self.style.layout("Treeview", [("Treeview.treearea", { 'sticky': 'nswe' })]) self.style.configure("Treeview.Heading", foreground='green', font='bold', stretch=tk.YES) self.tv.grid(row=3, column=2, columnspan=10, rowspan=6, pady=5, padx=10) self.tv.heading(1, text="Index") self.tv.column("1", minwidth=0, width=50, stretch=NO) self.tv.heading(2, text="Title") self.tv.column("2", minwidth=0, width=400, stretch=YES) self.tv.heading(3, text="Date") self.tv.column("3", minwidth=0, width=200, stretch=YES) self.tv.heading(4, text="Link") self.tv.column("4", minwidth=0, width=500, stretch=YES) self.tv.bind('<ButtonRelease-1>', self.select_item) rows = self.js.showTable(DatabaseName, name) for i in rows: self.tv.insert('', 'end', values=i) self.clearTextbox() # function to select row from list def select_item(self, a): global currentIndex global currentTitle global currentDate global currentLink global cleansummary global isSelected isSelected = True # print(isSelected) # rowSelection is set to the row the user left clicks on self.rowSelection = self.tv.selection()[0] cursel = self.tv.item(self.rowSelection) currentIndex = 0 currentIndex = cursel['values'][0] # print("The Current Index is: ", currentIndex) currentTitle = "" currentTitle = cursel['values'][1] # print("The Current Title is: ", currentTitle) currentDate = "" currentDate = cursel['values'][2] # print("The Current Date is: ", currentDate) currentLink = "" currentLink = cursel['values'][3] # print("The Current Link is: ", currentLink) cleansummary = "" # init the textbox self.SummaryBox.delete("1.0", "end") summary = cursel['values'][4] cleansummary = BeautifulSoup( summary, "lxml").text # clean summary text with BS4 self.SummaryBox.insert(tk.END, cleansummary) # function to delete an entry def deleteEntry(self): global isSelected if isSelected == True: if not DatabaseName == "" and not name == "": self.js.deleteDatabaseEntry(DatabaseName, name, currentIndex) self.showDB() else: pass else: pass print("isSelected was False") isSelected = False # region editDB window functions # function to create edit DB window: def editDBWindow(self): self.newWindow = tk.Toplevel(app) # Widget Title self.newWindow.title('Modify Database') # Width height self.newWindow.geometry("800x450") # change background color of root, color from https://html-color-codes.info/ self.newWindow.configure(bg="#585858") # set title bar icon self.newWindow.iconbitmap('Icons/RSS_Icon.ico') self.wfrm = tk.Frame(self.newWindow, bg="#585858") self.wfrm.pack() self.wFrame = tk.Label(self.wfrm, text="Modify Database Entry!", relief=GROOVE, anchor=CENTER, font='Times 12 italic', bg='#424242', fg='#FFFFFF') self.wFrame.grid(row=0, column=2, columnspan=2, pady=5, ipadx=10) self.new_title_label = Label(self.wfrm, text="Enter new Title:", bg='#424242', fg='#FFFFFF') self.new_title_label.grid(row=1, column=1, pady=5, padx=10) self.new_title_String = Entry(self.wfrm, width=45) self.new_title_String.grid(row=1, column=3, columnspan=1, pady=5) self.new_date_label = Label(self.wfrm, text="Enter new Date:", bg='#424242', fg='#FFFFFF') self.new_date_label.grid(row=2, column=1, pady=5, padx=10) self.new_date_String = Entry(self.wfrm, width=45) self.new_date_String.grid(row=2, column=3, columnspan=1, pady=5) self.new_URL_label = Label(self.wfrm, text="Enter new URL:", bg='#424242', fg='#FFFFFF') self.new_URL_label.grid(row=3, column=1, pady=5, padx=10) self.new_URL_String = Entry(self.wfrm, width=45) self.new_URL_String.grid(row=3, column=3, columnspan=1, pady=5) self.new_summary_label = Label(self.wfrm, text="Enter new Summary:", bg='#424242', fg='#FFFFFF') self.new_summary_label.grid(row=4, column=1, pady=5, padx=10) self.new_summary_Box = Text(self.wfrm, height=10, width=70) self.new_summary_Box.grid(row=5, column=1, columnspan=5, rowspan=6, pady=5, padx=20) self.saveChangesBtn = tk.Button(self.wfrm, text="Save Changes!", width=18, command=self.saveChanges) self.saveChangesBtn.grid(row=12, column=4, pady=10) self.deleteEntryBtn = tk.Button(self.wfrm, text="Quit!", width=18, command=self.quitWindow) self.deleteEntryBtn.grid(row=12, column=1, pady=10) # function to save changes to the database def saveChanges(self): global currentTitle global currentDate global currentLink global cleansummary global isSelected if isSelected == True: currentTitle = self.new_title_String.get() currentDate = self.new_date_String.get() currentLink = self.new_URL_String.get() cleansummary = self.new_summary_Box.get("1.0", END) self.new_title_String.delete(0, END) self.new_date_String.delete(0, END) self.new_URL_String.delete(0, END) self.new_summary_Box.delete("1.0", "end") # print("") # print("Database name is ",DatabaseName) # print("Table name is ", name) # print("Current Index: ",currentIndex) # print("new Title: ",currentTitle) # print("new Date: ",currentDate) # print("new URL: ",currentLink) # print("new Summary: ",cleansummary) self.js.updateTable(DatabaseName, name, currentIndex, currentTitle, currentDate, currentLink, cleansummary) self.showDB() else: pass print("isSelected was False") isSelected = False # function to quit the newwindow def quitWindow(self): self.newWindow.destroy() # endregion # function to test the values of all variables def testVar(self): print("URL is: ", entry) print("Name is: ", name) print("Database Name is: ", DatabaseName) self.js.testVariables()
class PytubeGUI: """ Minimalist GUI for pytube """ def __init__(self, root): self.master = root self.master.title("PyTube-GUI") self.master.iconbitmap("pylogo.ico") self.master.geometry("450x125") self.master.minsize(width=450, height=125) self.master.resizable(width=True, height=False) (width_offset, height_offset) = self.get_offsets() self.master.geometry(f"+{width_offset}+{height_offset}") self.master.update() #Widgets self.mode = StringVar(self.master) self.mode.set("Video + Audio") self.link_entry_x_scroll = Scrollbar( self.master, orient="horizontal" ) self.link_entry = Entry( self.master, font="Sans_Serif 12", xscrollcommand=self.link_entry_x_scroll.set ) self.link_entry_x_scroll.config(command=self.link_entry.xview) self.mode_menu = OptionMenu( self.master, self.mode, "Video + Audio", "Audio Only", "HQ" ) self.mode_menu.config(width=12) self.mode_label = Label( self.master, text="Mode:", font="Sans_Serif 10", ) self.status_label = Label( self.master, text="Ready", font="Sans_Serif 12", padx=10, relief="sunken" ) self.link_entry_button = Button( self.master, text="Submit", width=15, command=self.submit ) self.dir_entry_x_scroll = Scrollbar( self.master, orient="horizontal" ) self.dir_entry = Entry( self.master, font="Sans_Serif 12", xscrollcommand=self.dir_entry_x_scroll.set ) self.dir_entry_x_scroll.config(command=self.dir_entry.xview) self.dir_entry_button = Button( self.master, text="Select Directory", width=15, command=self.dir_select ) #HQ mode widgets: self.stream_list_y_scroll = Scrollbar( self.master, orient="vertical" ) self.stream_list = Listbox( self.master, yscrollcommand=self.stream_list_y_scroll.set ) self.stream_list_y_scroll.config(command=self.stream_list.yview) self.stream_selection = StringVar(self.master) #Layout: self.master.columnconfigure(0, weight=1) self.master.rowconfigure(5, weight=1) self.link_entry.grid(row=0, column=0, sticky="ew") self.link_entry_x_scroll.grid(row=1, column=0, sticky="ew") self.link_entry_button.grid(row=0, column=1) self.mode_label.grid(row=1, column=1, sticky="ew") self.mode_menu.grid(row=2, column=1) self.status_label.grid(row=2, column=0) self.dir_entry.grid(row=3, column=0, sticky="ew") self.dir_entry_x_scroll.grid(row=4, column=0, sticky="ew") self.dir_entry_button.grid(row=3, column=1) #Bindings, Traces, Protocols: self.link_entry.bind("<Return>", self.submit) self.stream_list.bind("<Return>", self.update_stream_selection) self.stream_list.bind("<Double-Button-1>", self.update_stream_selection) self.mode.trace("w", self.set_gui) self.master.bind("<Control-w>", self.close) self.master.protocol("WM_DELETE_WINDOW", self.close) #Probe ffmpeg: self.ffmpeg = which("ffmpeg") if not self.ffmpeg: messagebox.showwarning( "Error", "Unable to probe ffmpeg.\n\n"\ "HQ & Audio Only most likely will not function "\ "correctly without ffmpeg.exe on the system path." ) self.mode_menu.config(state="disabled") def __str__(self): """ Pretty print self w/ address """ return f"PyTube-GUI @ {hex(id(self))}" def set_gui(self, *args): """ Swap between GUI layouts (Stream list / No stream list) """ if self.mode.get() == "HQ": self.master.resizable(width=True, height=True) self.master.geometry("450x250") self.stream_list.grid(row=5,column=0, columnspan=2, sticky="nsew") self.master.resizable(width=True, height=False) else: self.master.resizable(width=True, height=True) self.master.geometry("450x125") self.stream_list.grid_remove() self.master.resizable(width=True, height=False) def hq_download(self, yt_vid, directory): """ Slower download rate on average, higher resolution + ffmpeg zip """ self.update_status_label("Select Stream") self.stream_list.delete(0,"end") streams = yt_vid.streams.filter( adaptive=True, type="video", subtype="mp4" ) for stream in streams: self.stream_list.insert("end",stream) self.master.update() self.master.wait_variable(self.stream_selection) if self.stream_selection.get() == "__NONE__": return self.stream_selection.get() itag = findall("itag=\"\d*\"", self.stream_selection.get()) itag = sub("itag=\"|\"","",itag[0]) hq_stream = yt_vid.streams.get_by_itag(itag) path = directory + "/" + clean_file_name(unescape(hq_stream.title)) + ".mp4" (filename, full_path) = name_file(path) #Try to get video stream first: hq_path = None try: hq_path = hq_stream.download(directory, filename=filename+" VIDEO") except Exception as err: messagebox.showwarning("Error", err) #If we have the video stream, then go for the audio as well: audio_path = None if hq_path: try: audio_path = yt_vid \ .streams \ .filter(only_audio=True, subtype="mp4") \ .order_by("abr") \ .desc() \ .first() \ .download(directory, filename=filename+" AUDIO") except Exception as err: messagebox.showwarning("Error", err) #If we get here with both, zip them: if hq_path and audio_path: run([ f"ffmpeg", "-i", hq_path, "-i", audio_path, "-codec", "copy", full_path ]) run(["del", hq_path],shell=True) run(["del", audio_path],shell=True) def download(self, yt_vid, directory): """ Quicker download, lower resolution """ stream = yt_vid.streams.filter( progressive="True", file_extension="mp4" ).first() path = directory + "/" + clean_file_name(unescape(stream.title)) + ".mp4" (filename, full_path) = name_file(path) try: stream.download(directory, filename=filename) except Exception as err: messagebox.showwarning("Error", err) else: if self.mode.get() == "Audio Only": #Audio only streams download rather slowly via pytube, #therefore, we take the audio in place w/ pydub: self.update_status_label("Extracting Audio...") audio = AudioSegment.from_file(full_path) audio.export(full_path, format="mp4") def disabled_ui(fn): """ Wrapper for submit() that prevents accidental repeat downloads. If we spam click submit, we'll end up with a bunch of duplicates, which probably isn't desirable. """ @wraps(fn) def inner(self, *args, **kwargs): self.link_entry_button.config(state="disabled") fn(self, *args, **kwargs) self.update_status_label("Ready") self.stream_list.delete(0,"end") self.link_entry.delete(0, "end") self.link_entry_button.config(state="normal") return inner @disabled_ui def submit(self): """ Controller/handler for download() & hq_download() """ if not self.link_entry.get(): return if not url(self.link_entry.get()): messagebox.showwarning( "Invalid Input", "Input is not a valid URL." ) self.link_entry.selection_range(0,"end") return self.update_status_label("Fetching") try: yt_vid = YouTube(self.link_entry.get()) except KeyError as err: return messagebox.showwarning("KeyError", err) except Exception as err: #Same as above, capture & return return messagebox.showwarning( "Invalid Link", f"Unable to fetch video.\n{err}" ) yt_vid.register_on_progress_callback(self.progress) directory = self.dir_entry.get() or self.dir_select() if not directory: self.update_status_label("Ready") return if not exists(directory): return messagebox.showwarning( "Invalid Path", "The specified directory cannot be found." ) if self.mode.get() == "HQ": self.hq_download(yt_vid, directory) else: self.download(yt_vid, directory) def close(self, event=None): """ Callback for window close, satisfies wait_variable() before killing the application. """ self.stream_selection.set("__NONE__") raise SystemExit def update_stream_selection(self,*args): """ Callback for Double-Click / Enter key on stream_list. Serves to unblock wait_variable in hq_download() """ self.stream_selection.set( self.stream_list.get(self.stream_list.curselection()) ) def dir_select(self): """ A wrapper for askdirectory() that populates the GUI. """ directory = askdirectory() if directory: self.dir_entry.delete(0, "end") self.dir_entry.insert(0, directory) return directory def update_status_label(self, status): """ DRYs up two function calls, adds some dynamic behavior. """ self.status_label.config(text=status) self.status_label.update() def progress(self, stream, chunk, bytes_remaining): """ Callback for download progress """ dl_progress = round( (100*(stream.filesize - bytes_remaining)) / stream.filesize, 2 ) self.update_status_label(f"Downloading... {dl_progress} %") def get_offsets(self): """ Returns an appropriate offset for a given tkinter toplevel, such that it always is created center screen on the primary display. """ width_offset = int( (self.master.winfo_screenwidth() / 2) - (self.master.winfo_width() / 2) ) height_offset = int( (self.master.winfo_screenheight() / 2) - (self.master.winfo_height() / 2) ) return (width_offset, height_offset)
class GUIChecker(Frame): def __init__(self, master=None): Frame.__init__(self, master) self.master = master self.grid(columnspan=2000) self.__set_default_value() self.__create_widgets() self.__locate_widges() self.setting_on = False def __set_default_value(self): self.ALL_FIELDS = ["系號", "序號", "餘額", "課程名稱(連結課程地圖)", "學分", "時間", "教師姓名*:主負責老師" "已選課人數", "教室", "選必修", "限選條件", "系所名稱", "年級", "組別", "類別", "班別", "業界專家參與", "英語授課", "Moocs", "跨領域學分學程", "備註", "課程碼", "分班碼", "屬性碼"] self.default_choosen_field = ["系號", "序號", "餘額", "課程名稱(連結課程地圖)", "學分", "時間", "教師姓名*:主負責老師"] self.choosen_field = deepcopy(self.default_choosen_field) def __create_widgets(self): self.input_label = Label(self, text="系所代碼 : ") self.input_field = Entry(self, width=30) self.search_btn = Button(self, text="搜尋", command=self.__search_method) self.clear_btn = Button(self, text="清除", command=self.__clear_method) self.setting_btn = Button(self, text="設定", command=self.__setting_method) self.msg_text = Label(self) def __locate_widges(self): self.input_label.grid(row=0, column=0) self.input_field.grid(row=0, column=1, columnspan=6) self.search_btn.grid(row=1, column=0) self.clear_btn.grid(row=1, column=2) self.setting_btn.grid(row=1, column=4) self.msg_text.grid(row=2, column=0) def __search_method(self): department_no = self.input_field.get() self.msg_text['text'] = '查詢中' try: self.__attach_course_table(department_no) except NoCourseAvailableError as e: logging.debug(e) self.msg_text['text'] = '沒有這個系所' except Exception as e: logging.exception('Seach Method: ') self.msg_text['text'] = '未知的錯誤' else: self.msg_text['text'] = '' def __attach_course_table(self, dept_no): courses = self.__search_courses(dept_no) title = list(courses.columns.values) self.__clear_method() self.__set_up_tree_widget(title, len(courses)) for field in title: self.tree.heading(field, text=field) self.tree.column(field, width=font.Font().measure(field)) for index, course in courses.iterrows(): self.tree.insert('', 'end', values=tuple(course.values)) for ix, val in enumerate(course.values): col_w = font.Font().measure(val) + 10 if self.tree.column(title[ix], width=None) < col_w: self.tree.column(title[ix], width=col_w) def __search_courses(self, dept_no): crawler = NckuCourseCrawler(dept_no=dept_no) html = crawler.get_raw_HTML() parser = NckuCourseParser(html) parser.include_fields = self.choosen_field logging.info("Choosen Field: {}".format(self.choosen_field)) courses = parser.parse(sort=True) print(courses) courses['餘額'] = courses['餘額'].apply(int) return courses def __clear_method(self): try: self.__remove_tree_widget() except Exception as e: logging.debug( 'Widget not yet created. Not and Error. {}'.format(e) ) def __remove_tree_widget(self): self.tree_vsb.grid_remove() self.tree_hsb.grid_remove() self.tree.grid_remove() def __set_up_tree_widget(self, title, courses_num): tree_height = min(30, courses_num) self.tree = ttk.Treeview(columns=title, show="headings", height=tree_height) self.tree_vsb = ttk.Scrollbar(orient="vertical", command=self.tree.yview) self.tree_hsb = ttk.Scrollbar(orient="horizontal", command=self.tree.xview) self.tree.configure(yscrollcommand=self.tree_vsb.set, xscrollcommand=self.tree_hsb.set) self.tree.grid(row=3, column=0) self.tree_vsb.grid(row=3, column=1, sticky="ns") self.tree_hsb.grid(row=4, column=0, sticky="we") def __setting_method(self): if not self.setting_on: self.setting_on = True self.setting_win = Toplevel(self) self.setting_win.wm_title("Settings") self.__create_setting_win_widget() def __create_setting_win_widget(self): choose_field_label = Label(self.setting_win, text="選擇欄位") self.__create_choose_field_listbox() confirm_btn = Button(self.setting_win, text="確定", command=self.__confirm_setting) default_btn = Button(self.setting_win, text="回復預設值", command=self.__restore_setting) confirm_btn.grid(row=0, column=0) default_btn.grid(row=0, column=1) choose_field_label.grid(row=1, column=0) def __create_choose_field_listbox(self): self.choose_field_listbox = Listbox(self.setting_win, selectmode=MULTIPLE, height=len(self.ALL_FIELDS)) for i in self.ALL_FIELDS: self.choose_field_listbox.insert(END, i) for index, choosen in enumerate(self.choosen_field): if choosen: self.choose_field_listbox.select_set(index) self.choose_field_listbox.grid(row=1, column=1, sticky="nsew") def __confirm_setting(self): print(self.choose_field_listbox) print(self.choose_field_listbox.curselection()) selections = self.choose_field_listbox.curselection() self.choosen_field = [ self.ALL_FIELDS[col_index] for col_index in selections ] self.setting_win.withdraw() self.setting_on = False def __restore_setting(self): self.choose_field_listbox.grid_remove() self.choosen_field = self.default_choosen_field self.__create_choose_field_listbox()