Файловый менеджер на питоне в 430 строк для начинающих и чайников

Всем привет!

Я захотел обобщить свои знания питона и решил написать файловый менеджер для пк.



Внимание!
Это всего лишь игрушка и не более, это не реальная ОС!

Импорт библиотек:

import tkinter
import os
import subprocess
from tkinter import messagebox
from tkinter import simpledialog

Главное меню:

class MainContextMenu(tkinter.Menu):
	''' Контекстное меню для внутренней области директории'''
	def __init__(self, main_window, parent):
		super(MainContextMenu, self).__init__(parent, tearoff = 0)
		self.main_window = main_window
		self.add_command(label="Создать директорию", command = self.create_dir)
		self.add_command(label="Создать файл", command = self.create_file)

	def popup_menu(self, event):
		''' функция для активации контекстного меню'''
		#если активны другие меню - отменяем их
		if self.main_window.file_context_menu:
			self.main_window.file_context_menu.unpost()
		if self.main_window.dir_context_menu:
			self.main_window.dir_context_menu.unpost()
		self.post(event.x_root, event.y_root)

	def create_dir(self):
		''' функция для создания новой директории в текущей'''
		dir_name = simpledialog.askstring("Новая директория", "Введите название новой директории")
		if dir_name:
			command = "mkdir {0}".format(dir_name).split(' ')
			#выполняем команду отдельным процессом
			process = subprocess.Popen(command, cwd=self.main_window.path_text.get(), stdout = subprocess.PIPE, stderr = subprocess.PIPE)
			out, err = process.communicate()
			#при возникновении ошибки выводим сообщение
			if err:
				messagebox.showwarning("Операция невозможна!","Отказано в доступе.")
			self.main_window.refresh_window()


	def create_file(self):
		''' функция для создания нового файла в текущей директории'''
		dir_name = simpledialog.askstring("Новый файл", "Введите название нового файла")
		if dir_name:
			command = "touch {0}".format(dir_name).split(' ')
			#выполняем команду отдельным процессом
			process = subprocess.Popen(command, cwd=self.main_window.path_text.get(), stdout = subprocess.PIPE, stderr = subprocess.PIPE)
			out, err = process.communicate()
			#при возникновении ошибки выводим сообщение
			if err:
				messagebox.showwarning("Операция невозможна!","Отказано в доступе.")
			self.main_window.refresh_window()



	def insert_to_dir(self):
		''' функция для копирования файла или директории в текущую директорию'''
		copy_obj = self.main_window.buff
		to_dir = self.main_window.path_text.get()
		if os.path.isdir(self.main_window.buff):
			#выполняем команду отдельным процессом
			process = subprocess.Popen(['cp', '-r', copy_obj, to_dir], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
			out, err = process.communicate()
			if err:
				messagebox.showwarning("Операция невозможна!", err.decode("utf-8"))
		else:
			#выполняем команду отдельным процессом
			process = subprocess.Popen(['cp', '-n', copy_obj, to_dir], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
			out, err = process.communicate()
			#при возникновении ошибки выводим сообщение
			if err:
				messagebox.showwarning("Операция невозможна!",err.decode("utf-8"))
		self.main_window.refresh_window()

При нажатии на файл должно выводиться контекстное меню:

image

class FileContextMenu(tkinter.Menu):
	def __init__(self, main_window, parent):
		super(FileContextMenu, self).__init__(parent, tearoff = 0)
		self.main_window = main_window
		self.add_command(label="Открыть файл", command = self.open_file)
		self.add_separator()
		self.add_command(label="Копировать", command = self.copy_file)
		self.add_command(label="Переименовать", command = self.rename_file)
		self.add_separator()
		self.add_command(label="Удалить", command = self.delete_file)


	def open_file(self):
		''' функция для открытия файла сторонними программами'''
		ext = self.main_window.take_extention_file(self.main_window.selected_file)
		full_path = self.main_window.path_text.get() + self.main_window.selected_file

		if ext in ['txt', 'py', 'html', 'css', 'js']:
			if 'mousepad' in self.main_window.all_program:
				subprocess.Popen(["mousepad", full_path], start_new_session = True)
			else:
				self.problem_message()
		elif ext == 'pdf':
			if 'evince' in self.main_window.all_program:
				subprocess.run(["evince", full_path], start_new_session = True)
			else:
				self.problem_message()
		elif ext in ['png', 'jpeg', 'jpg', 'gif']:
			if 'ristretto' in self.main_window.all_program:
				subprocess.run(["ristretto", full_path], start_new_session = True)
			else:
				self.problem_message()
		else:
			self.problem_message()

	def problem_message(self):
		messagebox.showwarning("Проблема при открытии файла", 'Прости, но я не могу открыть этот файл')

	def copy_file(self):
		''' функция для копирования файла'''
		#заносим полный путь к файлу в буффер
		self.main_window.buff = self.main_window.path_text.get() + self.main_window.selected_file
		self.main_window.refresh_window()


	def delete_file(self):
		''' функция для удаления выбранного файла'''
		full_path = self.main_window.path_text.get() + self.main_window.selected_file
		#выполняем команду отдельным процессом
		process = subprocess.Popen(['rm', full_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		output, err = process.communicate()
		#при возникновении ошибки выводим сообщение
		if err:
			messagebox.showwarning("Проблема при удалении файла", 'У Вас нет прав для удаления данного файла')
		self.main_window.refresh_window()

	def rename_file(self):
		''' функция для переименования выбранного файла'''
		new_name = simpledialog.askstring("Переименование файла", "Введите новое название файла")
		if new_name:
			old_file = self.main_window.path_text.get() + self.main_window.selected_file
			new_file = self.main_window.path_text.get() + new_name
			#выполняем команду отдельным процессом
			process = subprocess.Popen(['mv', old_file, new_file], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
			output, err = process.communicate()
			#при возникновении ошибки выводим сообщение
			if err:
				messagebox.showwarning("Проблема при переименовании файла", 'У Вас нет прав для переименования данного файла')
			self.main_window.refresh_window()

	def popup_menu(self, event):
		''' функция для активации контекстного меню'''
		self.post(event.x_root, event.y_root)
		#если активны другие меню - отменяем их
		if self.main_window.main_context_menu:
			self.main_window.main_context_menu.unpost()
		if self.main_window.dir_context_menu:
			self.main_window.dir_context_menu.unpost()
		self.main_window.selected_file = event.widget["text"]


То же самое для директории:

class DirContextMenu(tkinter.Menu):
	def __init__(self, main_window, parent):
		super(DirContextMenu, self).__init__(parent, tearoff = 0)
		self.main_window = main_window
		self.add_command(label="Переименовать", command = self.rename_dir)
		self.add_command(label="Копировать", command = self.copy_dir)
		self.add_separator()
		self.add_command(label="Удалить", command = self.delete_dir)

	def copy_dir(self):
		''' функция для копирования директории'''
		self.main_window.buff = self.main_window.path_text.get() + self.main_window.selected_file
		self.main_window.refresh_window()


	def delete_dir(self):
		''' функция для удаления выбранной директории'''
		full_path = self.main_window.path_text.get() + self.main_window.selected_file
		if os.path.isdir(full_path):
			#выполняем команду отдельным процессом
			process = subprocess.Popen(['rm', '-rf', full_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
			output, err = process.communicate()
			#при возникновении ошибки выводим сообщение
			if err:
				messagebox.showwarning("Проблема при удалении директории", 'У Вас нет прав для удаления данной директории')
		self.main_window.refresh_window()

	def rename_dir(self):
		''' функция для переименования выбранной директории'''
		new_name = simpledialog.askstring("Переименование директории", "Введите новое название директории")
		if new_name:
			old_dir = self.main_window.path_text.get() + self.main_window.selected_file
			new_dir = self.main_window.path_text.get() + new_name
			#выполняем команду отдельным процессом
			process = subprocess.Popen(['mv', old_dir, new_dir], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
			output, err = process.communicate()
			#при возникновении ошибки выводим сообщение
			if err:
				messagebox.showwarning("Проблема при переименовании директории", 'У Вас нет прав для переименования данной директории')
			self.main_window.refresh_window()

	def popup_menu(self, event):
		''' функция для активации контекстного меню'''
		self.post(event.x_root, event.y_root)
		#если активны другие меню - отменяем их
		if self.main_window.main_context_menu:
			self.main_window.main_context_menu.unpost()
		if self.main_window.file_context_menu:
			self.main_window.file_context_menu.unpost()
		self.main_window.selected_file = event.widget["text"]


Класс основного окна:


class MainWindow():
	''' Класс основного окна'''
	def __init__(self):
		self.root = tkinter.Tk()
		self.root.title("FileManager")
		self.root.resizable(width = False, height = False)
		self.root.geometry('450x300')

		self.hidden_dir = tkinter.IntVar()
		self.buff = None
		self.all_program = os.listdir('C:/')

		self.root.bind('<Button-1>', self.root_click)
		self.root.bind('<FocusOut>', self.root_click)

		#top frame
		self.title_frame = tkinter.Frame(self.root)
		self.title_frame.pack(fill = 'both', expand = True)

		#back button
		self.back_button = tkinter.Button(self.title_frame, text = "..", command = self.parent_dir, width = 1, height = 1)
		self.back_button.pack(side = 'left')

		#path entry
		self.path_text = tkinter.StringVar()
		self.path_text.set('/')
		self.current_path = tkinter.Entry(self.title_frame, textvariable = self.path_text, width = 40, state='readonly')
		self.current_path.pack(side = 'left')

		#button show/hidde hidden dir/file
		self.check_button = tkinter.Checkbutton(self.title_frame, text = "Hidden", font = ("Helvetica", 10), padx = 1, pady = 1, variable = self.hidden_dir, command = self.refresh_window)
		self.check_button.pack(side = 'left')

		#main frame
		self.main_frame = tkinter.Frame(self.root)
		self.main_frame.pack()

		# scroll bar
		self.scrollbar_vert = tkinter.Scrollbar(self.main_frame, orient="vertical")
		self.scrollbar_vert.pack(side = 'right', fill = 'y')

		self.scrollbar_hor = tkinter.Scrollbar(self.main_frame, orient="horizontal")
		self.scrollbar_hor.pack(side = 'bottom', fill = 'x')

		#canvas
		self.canvas = tkinter.Canvas(self.main_frame, borderwidth=0,  bg = 'white')
		self.inner_frame = tkinter.Frame(self.canvas,  bg = 'white')

		#команды для прокрутки
		self.scrollbar_vert["command"] = self.canvas.yview
		self.scrollbar_hor["command"] = self.canvas.xview

		#настройки для canvas
		self.canvas.configure(yscrollcommand=self.scrollbar_vert.set, xscrollcommand = self.scrollbar_hor.set, width=400, heigh=250)

		self.canvas.pack(side='left', fill='both', expand=True)
		self.canvas.create_window((0,0), window=self.inner_frame, anchor="nw")


		#отрисовываем содержимое лиректории
		self.dir_content()


	def root_click(self, event):
		''' функция для обработки события клика в root'''
		#если есть контекстные меню - отменяем
		if self.file_context_menu:
			self.file_context_menu.unpost()
		if self.main_context_menu:
			self.main_context_menu.unpost()
		if self.dir_context_menu:
			self.dir_context_menu.unpost()

	def dir_content(self):
		''' функция для определения содержимого текущей директории'''
		#содержимое в текущей директории
		dir_list = os.listdir(self.path_text.get())
		path = self.path_text.get()

		if not dir_list:
			#общее контекстное меню
			self.main_context_menu = MainContextMenu(self, self.canvas)
			self.canvas.bind('<Button-3>', self.main_context_menu.popup_menu)
			if self.buff:
				self.main_context_menu.add_command(label="Вставить", command = self.main_context_menu.insert_to_dir)
			self.inner_frame.bind('<Button-3>', self.main_context_menu.popup_menu)
			#контекстное меню для файлов
			self.file_context_menu = None
			#контекстное меню для директории
			self.dir_context_menu = None
			return None

		#общее контекстное меню
		self.main_context_menu = MainContextMenu(self, self.canvas)
		self.canvas.bind('<Button-3>', self.main_context_menu.popup_menu)
		if self.buff:
			self.main_context_menu.add_command(label="Вставить", command = self.main_context_menu.insert_to_dir)
		#контекстное меню для файлов
		self.file_context_menu = FileContextMenu(self, self.inner_frame)
		#контекстное меню для директории
		self.dir_context_menu = DirContextMenu(self, self.inner_frame)


		i = 0
		for item in dir_list:

			if os.path.isdir(str(path) + item):
				#обрабатываем директории
				if os.access(str(path) + item, os.R_OK):
					if (not self.hidden_dir.get() and  not item.startswith('.')) or self.hidden_dir.get():
						photo = tkinter.PhotoImage(file ="img/folder.png")

						icon = tkinter.Label(self.inner_frame, image=photo,  bg = 'white')
						icon.image = photo
						icon.grid(row=i+1, column=0)

						folder_name = tkinter.Label(self.inner_frame, text=item,  bg = 'white', cursor = 'hand1')
						folder_name.bind("<Button-1>", self.move_to_dir)
						folder_name.bind("<Button-3>", self.dir_context_menu.popup_menu)
						folder_name.grid(row=i+1, column=1, sticky='w')
				else:
					if (not self.hidden_dir.get() and not item.startswith('.')) or self.hidden_dir.get():
						photo = tkinter.PhotoImage(file ="img/folder_access.png")

						icon = tkinter.Label(self.inner_frame, image=photo,  bg = 'white')
						icon.image = photo
						icon.grid(row=i+1, column=0)

						folder_name = tkinter.Label(self.inner_frame, text=item,  bg = 'white')
						folder_name.bind("<Button-1>", self.move_to_dir)
						folder_name.grid(row=i+1, column=1, sticky='w')

			else:
				#обрабатываем файлы
				if (not self.hidden_dir.get() and not item.startswith('.')) or self.hidden_dir.get():
					ext = self.take_extention_file(item)
					#фото, картинки
					if ext in ['jpeg', 'jpg', 'png', 'gif']:
						photo = tkinter.PhotoImage(file ="img/photo.png")

						icon = tkinter.Label(self.inner_frame, image=photo,  bg = 'white')
						icon.image = photo
						icon.grid(row=i+1, column=0)

						file_name = tkinter.Label(self.inner_frame, text=item,  bg = 'white')
						file_name.grid(row=i+1, column=1, sticky='w')

						file_name.bind("<Button-3>", self.file_context_menu.popup_menu)
					else:
						#другие файлы
						if os.access(str(path) + item, os.R_OK):
							photo = tkinter.PhotoImage(file ="img/file.png")

							icon = tkinter.Label(self.inner_frame, image=photo,  bg = 'white')
							icon.image = photo
							icon.grid(row=i+1, column=0)

							folder_name = tkinter.Label(self.inner_frame, text=item,  bg = 'white')
							folder_name.grid(row=i+1, column=1, sticky='w')

							folder_name.bind("<Button-3>", self.file_context_menu.popup_menu)

						else:
							photo = tkinter.PhotoImage(file ="img/file_access.png")

							icon = tkinter.Label(self.inner_frame, image=photo,  bg = 'white')
							icon.image = photo
							icon.grid(row=i+1, column=0)

							folder_name = tkinter.Label(self.inner_frame, text=item,  bg = 'white')
							folder_name.grid(row=i+1, column=1, sticky='w')
			i += 1
		#обновляем inner_frame и устанавливаем прокрутку для нового содержимого
		self.inner_frame.update()
		self.canvas.configure(scrollregion=self.canvas.bbox("all"))

	def move_to_dir(self, event):
		''' функция для перехода в выбранную директорию'''
		elem = event.widget
		dir_name = elem["text"]
		fool_path = self.path_text.get() + dir_name
		if os.path.isdir(fool_path) and os.access(fool_path, os.R_OK):
			old_path = self.path_text.get()
			self.path_text.set(old_path + dir_name + '/')
			self.root_click('<Button-1>')
			self.refresh_window()


	def parent_dir(self):
		''' функция для перемещения в родительскую директорию'''
		old_path = [i for i in self.path_text.get().split('/') if i]
		new_path = '/'+'/'.join(old_path[:-1])
		if not new_path:
			new_path = '/'
		if os.path.isdir(new_path):
			if new_path == '/':
				self.path_text.set(new_path)

			else:
				self.path_text.set(new_path + '/')
			self.refresh_window()


	def take_extention_file(self, file_name):
		''' функция для получения расширения файла'''
		ls = file_name.split('.')
		if len(ls)>1:
			return ls[-1]
		else:
			return None

	def refresh_window(self):
		''' функция для обновления текущего отображения директорий/файлов'''
		for widget in self.inner_frame.winfo_children():
				widget.destroy()
		self.dir_content()
		self.canvas.yview_moveto(0)

И наконец, создание окна и запаковка виджетов:

win = MainWindow()
win.root.mainloop()

Файлы, ассеты, бинарники здесь

Буду рад, если вы поделитесь со мной улучшенной версией этой программы!
Пишите: ki1killer@yandex.ru
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 21

    +7
    Файловые менеджеры (как и другие прикладные программы) не делают на вызовах команд оболочки (shell), для этого системные/библиотечные вызовы есть
      +12
      Вместо сплошной простыни текста исходника, лучше исходники кинуть на гитхаб, а тут написать статью как вы дошли до такого, указать трудности и запостить в какой-нить тег типа tutorial
      А так — просто кусок текста на питоне, на статью не похож совсем.

      Плюс серьезно — у вас там захардкоженый C:\ и при этом использование линукс консольных команд для файловых операций?
        +2

        +1. Простыня кода без пояснений — зачем она? Использовать вместо файл-менеджера его мало кто будет, при наличии готовых более функциональных. Использовать вместо учебного пособия? Так непонятно, чему автор хочет научить, как он до этого дошел и какие сделал выводы. До ненормального программирования это.не дотягивает.

          +2

          Посмотрел в профиль, немного понял ;). Ki1killer, в питоне есть и другие модули, кроме вот этого tkinter ;). shutils например.

        +5
        command = "mkdir {0}".format(dir_name).split(' ')
        попробуйте создать каталог, содержащий пробелы в имени
          +3
          Не пишите посты которые не объясняют код, которые являются пустым набором кода. Пишите так, чтобы было интересно читать даже не глядя в код.
          Вот отличные примеры у которых стоит поучиться:
          Пишем примитивный и никому не нужный компилятор
          Первая игра, которую я просто написал для себя
          Как я написал приложение, которое за 15 минут делало то же самое, что и регулярное выражение за 5 дней
            –15

            Заранее извиняюсь, но у меня подгорело.
            Зачем делать это на скриптовом языке. Он не предназначен для этого. Я наверное старовер уж извините, но считаю что для каждого языка есть своё предназначение. Если мы будем писать на чем угодно что угодно, тогда мы и будем получать костыльные ситуации из серии nosql сервер баз данных на JS во вкладке браузера…
            Для поиграться сойдет, но я не уверен что этим нужно делиться сообществом.
            Сам в свое время игрался и рисовал windows forms, дергая системные вызовы из dll но понял что использовать это — путь в никуда.

              +2

              "Зачем делать это на скриптовом языке. Он не предназначен для этого. "


              А что не так? У языка есть биндинги к gui, базам данных, к чему угодно, есть встроенная поддержка разных структур данных.

                +2
                Именно так. В файловом менеджере нет ничего такого, что требовало бы высокой производительности. Ну, скажем так: обычно нет — если подсунуть ему папку с парой миллионов файлов, скорее всего средний файловый менеджер сломается.

                Но вообще это была бы наверное последняя из претензий к изложенному тут решению.
              –16

              TensorFlow на JS, докер на Go, файловый менеджер на питоне, что дальше? ОС на Lua?

                +7

                А что не так с докером на го?

                  –12

                  Всё. Загляните в его исходный код, или в бенчмарки на системах, в которые не завезли нативные контейнеры (Win, Mac).


                  Го слабо (никак) предназначен для таких задач.

                    +6

                    Кек. Так там же проблема не в докере и го, а в отсутствии нативных контейнеров :). Докер это просто фронтенд, он бы одинаково быстро работал даже и на bash и на cmd.

                  +1
                  ОС на Lua

                  Так уже: habr.com/ru/post/272391
                    0
                    Отличный пример, главное имеет практический смысл. Но Lua в пространстве кернела Linux действительно не очень. Поэтому согласен с предыдущим оратором — Lua лучше запускать на голом гипервизоре аппаратном.
                    0
                    А вы миропроцессоры проектировали, чтоб про ОС рассуждать?
                      0

                      Прочтите дальше заголовка, там гораздо веселее чем очередной жс срач!) Это реально из серии про грабят корованы хD

                    • UFO just landed and posted this here
                        +5
                        Я думаю если бы статья была «PEP8 для начинающих и чайников», то даже в плюс бы вышли.
                          0

                          Автор молодец что учится, но этому не место на Хабре. Это надо сначала на разбор полетов, что не так в этом коде и посте, а потом разобрать по цитатам и в сборник вредных советов.


                          А вообщем кто может посоветовать автору какой то чатик или другой ресурс где будут делать бесплатный код ревью?

                        Only users with full accounts can post comments. Log in, please.