Сборка приложения на Python 2.7 + PyQt4 в один exe файл под Windows

Наверное у каждого человека, который пишет на Python возникал вопрос: "А как моё приложение могут запустить другие пользователи?". Разумеется, можно поставить Python и все необходимые библиотеки, а можно просто скомпилировать всё в один exe файл. Как по мне - второй вариант намного проще. А используя PyQt4 можно ещё и неплохой GUI рисовать. Для примера я покажу как собрать небольшую программу PassGen - генератор паролей.

Итак, нам понадобятся:
- Python 2.7
- PyQt4
- py2exe
- png2ico

Качаем, ставим, настраиваем. Рекомендую прописать папку установки Python в переменную системы %PATH%, что бы из консоли работала команда python script.py без указания полного пути до python.exe.

Все скрипты для сборки, включая сам генератор PassGen.pyw будут в архиве с конце статьи.

Начнём с подготовки. Мы же хотим, что бы у нашего приложения была красивая иконка, которая правильно показывается на всех версиях Windows от XP до 10? Значит нам понадобится её изготовить. Ну или просто позаимствовать. Для того, что бы собрать универсальный ico файл нам понадобятся пять картинок в формате png с разрешением 16x16, 32x32, 48x48, 64x64 и 128x128. Сохраняем их в папке icons. Из этих картинок наш скрипт соберёт правильную иконку. Далее есть ньюанс с иконками в самом приложении: их желательно вставить в тело самого pyw скрипта. Как? Очень просто - закодировать их в base64.

Всё подготовили? Написали pyw скрипт, который работает как надо и не подгружает внешние рессурсы? Тогда начинаем собирать. Подготавливаем скрипт для сборки через py2exe:

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Импорт компилятора и настройщика
import py2exe
from distutils.core import setup
# Версия готового .exe файла.
# Должна состоять из 4 групп цифр. Например: 1.0.0.0
VER_FILE = 'version.txt'
VER = file(VER_FILE).read().strip()
# Подготовка к сборке
setup(
# Опции сборки.
# includes - подключаемые библиотеки
# для PyQt4 обязательно надо включать в сборку библиотеку sip
# bundle_files - собрать все библиотеки в один zip файл
# compressed - сжать готовый файл
options = {'py2exe': {"includes":["sip"], 'bundle_files': 1, 'compressed': True}},
# None - включить library.zip в .exe файл
zipfile = None,
# Версия .exe файла
version = VER,
# Название .exe файла
name = 'PassGen',
# Описание
description = 'Password Generator',
windows = [{
# Название скрипта для компиляции
"script":"PassGen.pyw",
# Пакет иконок приложения
"icon_resources": [(1, "icons/PassGen.ico")],
# Исходное имя файла
"dest_base":"PassGen",
# Копирайты
"copyright":'Copyright (C) diSabler <dsy@dsy.name>',
# Название компании производителя
"company_name": 'Disabler Production Lab.'
}],
)
# The end is near!

Сохраняем его в build.py. Рядом с ним ложим файл version.txt в котором указываем версию своего скрипта. Теперь напишим простенький cmd файл, который автоматизирует всю ручную работу:

@echo off
rem *** Очищаем папку dist, если она осталась от прошлой сборки
rmdir dist /S /Q
rem *** Генерируем ico файл из наших png иконок
cd icons
del PassGen.ico
..\png2ico.exe PassGen.ico PassGen128.png PassGen64.png PassGen48.png PassGen32.png PassGen16.png
cd ..
rem *** Собираем exe файл
python build.py py2exe
rem *** Чистим мусор
del dist\w9xpopen.exe
rem *** Ждём и чистим временную папку сборки
ping -n 3 127.0.0.1 > nul
rmdir build /S /Q
ping -n 3 127.0.0.1 > nul
rmdir build /S /Q
rem *** Всё готово. Ждём нажатия любой клавиши
echo Finished!
pause
rem *** The end is near!

Сохраняем полученный файл в build.cmd.

Ну и сам скрипт генерации паролей:

#!/usr/bin/python
# -*- coding: utf-8 -*-
# --------------------------------------------------------------------------- #
# #
# Password Generator #
# Copyright (C) diSabler <dsy@dsy.name> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
# #
# --------------------------------------------------------------------------- #
# execute in npp: cmd /K python "$(FULL_CURRENT_PATH)"
from PyQt4 import QtGui, QtCore, Qt
import random
import sys
import base64
# Base64 data
app_logo ='''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAB
JAAAASQBIAXRoQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAMqSURBVFiFxZdN
aFNLGIafyUlI2vyo2NQ2jUqyKElpiy5U1IV/6b0Lae3ShbgQCxfusjst0oXgQgIiLpTaxp0LhVJ0ZZS6
UexG7b2lEAQxoYE0SUWx8Z7DPT1x0bT+NKednP74wsDAmXm/Z+abmTMjrsEODfqBPqCRrVEeGHJC3F4J
fnmLAi+pEbisAcpxuA+4rbj4gkGEovC/qiKsgUTs1Djt3kCAruvXCcdiuBsXu86l07x7/JjnAwNonz7V
YtcoBqEsjdvby5lEAtf27VW/F3M5Hp4/Ty6ZlJ4RaQBvSwt/T0/j9PkAyGQyjI6Oomka3d3dRKNRAD7O
zXGlrY2GfF4KQjkOgzIAvffu0bRvHwDJZJJTJ07wfmwM7elTnt29iyscJtLRQV19PXooxPsHD3BI+ErN
gL2ujkulEkIIVFWlLRKhNZ3mECyPUvF4+CuVwh8IUC6XOenzcWx+fs1ZsElAsrO1FSEWrcbHx/maTnOg
0llUijE/zz+JxOKohMDR3i6VWymAhkp+AVKpFK2AUqXd3Nu3y/VwZyfqRgF4du1ars/mcvhN2n3JZpfr
zc3N5Fk7v1IAiO+ZNAyDeol2Qgj+k7CWA/hBhmGYLixh+25XRm5/bxrAEsRaMt2G4a4u/ojH2bZnD4rT
icPlAkBVVQxNqwphUxScHg8AmqahqyqfMxme9PfzweR0NAW48OIFu48ckRjD2pp4+ZLbR4+yF1ZAmKbA
4bb0g6wql9vNvybf7FZNS6USw8PDzOZyXOzrIxQKrdp+YaMBumIxZl694jBw89Ej4pOT2Gw1r+nadwFA
oVBgcmKCc0AU2DY1RXZqyoqVNQC/309s/34cLC4qb1MTwfb2rQMAuJpIUKos1LNjYyvOgE0H6Ojs5JnX
iwG0HDxo1cY6AEBNt7/NANgIWd6GAKd7eogUCr8P4NadO+sKDqukoLxgdnbVroVVvEwBXo+MoOv6uoPr
uk5iZMT0hiwGYZYqr6MyMN3QQDYYlLrbmSk7MwPFIj1AgBV/w7wdGKLK41QAbcUi0WJxHeF/9qtyHxhS
/oQ3C6ADEX55pIoNLj8oD9xwQvwbg73vmZpeo3MAAAAASUVORK5CYII='''
refresh_icon ='''
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAB
OwAAATsBH99vcQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAFKSURBVDiNldOx
apRREAXgbzYSkkII26SJmFjsC1jZ29mvxDewW8gL+BA+gwRSWEgwnZ0SMS8gBCVFzAPIsophUty55Eey
gR04/Nz7zzlz7505kZmGERETvMLTwjW+FI4z83vlBTZkphIJzDBHFn7iYrBe4DV28BF7Q/JxJV1iivFA
fBv7+FU5v3GVmXrCrH6cDIlDYB1v60qJ0379SR37chm5Eg/wCef4g0NswJtSnC4j3yH2CC8LPpTA0up3
CGwV5zDq6H8zc9cKERE/sDbCv1JbNQIPRviM3YjYWqH6GI9xNsJ7rccPV6j+vL7faK040lpzrrXq4J4H
HGvvNsekb37V3uFaG5b1e8gnlTvrgxS40sYztXHdx/Z/xGlVTm3sows80Yyxoxll4dY8F5qh+nqujX10
8cAmFtld1ez8As8KazgrvOt27nEDRHALvH3Sl1QAAAAASUVORK5CYII='''
exit_icon='''
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMA
AAsSAAALEgHS3X78AAAAB3RJTUUH3wgMCToBCGUlQwAAArJJREFUOMuNkk2IVWUYx3/Pe95z7jmncZyQ
SIQZCi1QB6XAICoJoiBKKvrcGi2CCAMpWpYR1SIoEDdF1KJFu6BFmyhTwiyNotuEEyTd6Y7N98e998z5
eN/3aTGWo7no/+dZPt8/ab/9gJZFUQwq1wMEBUUDqoqqKoTNmb22O7v4zIGj7U8AZYNsaGqGctv64LuZ
I8c+m/gKqIECGAB9YPX71+95YSg18ZXJAEaDYhDz8n2jb75/6M5ngQTYBAwDm4GRVqPJF9dFj469OnZA
jBjkks1SyAnNmvT6Fffu2HTwyX1bHwbkYgBUR0YWt/xyx/aH1rrFogYN6CXb4bCCqhC8Z2lxlemZ1WzD
hOWut3Y97/aMvmjWau5+Yv+X8rhobCzWWECxzivBBDQo3gciI3Jx12L8jfGXbtq3/bAEKPyAKlRxZAwp
CalJyaIM612gCjViIrz3631FTHSr2b3zrpsPzy3P6VK9IHmckce5pDYhsxmpbRGLxTjncE5p6gbnFNSD
aghnw+Tx904+shKWJbO55nFOYpLQkiS0JA6JJCGRJNimcWSZoaoDtcRkY3vhj9Moauc/nv/BpNy++7Gd
p3JJoTYTvqbf2ICYhhqH7YWUtC6IreGbP2GlKP85oKhT5j5cODfO9KGFp/e+e/Kjb5/rHOueuIwDyj5N
4yjLiluGe2S9zmWgaBW4Px8Z2vHOmVfYEonIv+9dJzEODVUZ1BiRsvJYDRsZWJfzcmPNVOe1ztdXkhhN
TA8+vW1b6ymEtGm8/PxXcWJyvjoLNIAHzJnzy7+enip+mh24tf+gPLVStz+f7O+py0acDygMAdcDW4Ft
QHFurjw/MVsucRWZ+cL5H7uDbnumejCLBNBrgFFgFTgFLAMEVb1agQjgQt9x/Pf+b/tvyM2FnjftmfIo
0OF/6G/szVaXzVnmXQAAAABJRU5ErkJggg=='''
pass_easy ='''
iVBORw0KGgoAAAANSUhEUgAAAD8AAAAQCAYAAAChpac8AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAB
QwAAAUMBH0gTIwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAFaSURBVEiJ7Zc/
bsIwGEefwYIIRAgz/2aWDB2YkSKOAFfhDJwlRwAuwJiZMWIhEgIRIBCaDi1VvTpBpQ1PsmR/st7n32LZ
AG/AAkhyNBZfuZk9wWF+Y8wE8A6I6XSKbdv8dzzPYzKZACQSEAD9fp/BYABBAL6vb+92odEAIIoiwjDU
VlWrVcrlMgCXy4XD4aDtqlQqGIaBYRj3kpDKjiCAVguiSLsJpgnrNZGUuK5LHMfaKikl4/EYIQSu63K9
XrVdxWKR0Wik+pWV76cLDrDfw2ZDaJqpggPEcczpdAJIFRzgdrtxPB6VWiGV8Y/zCp9XXuG/ESIba1ae
B6Pe9r0eOA6sVvpG24ZOBytJaDab7HY7bVW9Xsc0TQDa7Tbb7VbbVavVsCxLqanhSyWYz7Ub/KQgBMPh
MBMXgOM4mbnuSD7fuWK5XHI+nzNv8Gx4nnefJpDjjw3k+Ev7AbNcVXdbKNltAAAAAElFTkSuQmCC'''
pass_medium ='''
iVBORw0KGgoAAAANSUhEUgAAAD8AAAAQCAYAAAChpac8AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAB
QwAAAUMBH0gTIwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAFYSURBVEiJ7Zcx
TsMwFIY/x6WKIrUKohMiZWGAJQND50oZOEHP0jP0LL0AUukFOmZiYIKqDLRUQlUUGmIztBV4YHGCqEh/
6Ul+T/b33j/ZBrgGxoCuUIy3vhntwTB/ESMBKEAMBgPCMOS/K45j+v0+gK4BAqDT6dDtdkHNQU3t6fIc
xPFmrV5BPdqznACcEwDW6zWr1coa5Xkeruviuu6uJGrGDjWHxRnod+smiCa0ZqBTWLRBJwVYHrRmZB8e
w+GQLMusUVJKer2eUXOMTE2LGQfQb6BeQD0VMw6b8+qZJEkKGQfI85wkMedxfthbCR3MV1UH818SJWFF
yazfkXnVySuoR5A/FCCGINvAKdRvIL+3Z8lLkBc0mw5BELBcLq1RjUYD3/fNUY1M1MG/s25g6gj821JI
QkAURaWwvqvG5p0rJpMJaZqW3mDfFMfxbqmhwh8bqPCX9hNDeFFQdgL9/gAAAABJRU5ErkJggg=='''
pass_strong ='''
iVBORw0KGgoAAAANSUhEUgAAAD8AAAAQCAYAAAChpac8AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAB
QwAAAUMBH0gTIwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAGBSURBVEiJ7Zc9
T8JAHIefwyK1MSYIE+HFgQGWRh1YWEgYWPg+fAa+jg6aoCMMjGVhYAERBgkQNbWVNwfAUN2uTUTr73LJ
5dI8/3tyl7YHcAncAysf9fuNN7U9WMxP9JoAloCoVqvous5fj2EYVCoVgJUCCIBcLkehUGA0G9G3+9Lw
lJoirIQBGM/G9OyeNCsRShAJRgCYzCd0ra40Kx6KEw1GUVV1OyWU3QdGsxGJehx7aUsXOVFOeMwPsJYW
qUYSc2FKs7QDjcf8AIHgrJ7iZfEizQoFQjzknZvqkO/bfVfiAM/zZ55mT0znU1fiAObCZPg+BHAlDmAv
7W8nOuCK+MvzL+/X+Fre8cIT66+e64hN84rlVb6yHPJZLUsxXKTz1pEuoB/rJNUksVWM0mmJttmWZmW0
DOmjNADlaJnWa0ualT5Kk9WyNGh8zjnkDwOH1C7upAvsJiiC3JzfesICuNKvPWNto7D+zxXNZhPLsjwv
sG8xDGM7XIGPLzbg4yvtBxopZlXG8xCuAAAAAElFTkSuQmCC'''
two_en = ['qa', 'qe', 'qi', 'qo', 'vu', 'wu', 'xo', 'xu', 'yu', 'zi', 'zu']
MIN_MEDIUM_SIZE = 6
MAX_MEDIUM_SIZE = 32
MIN_STRONG_SIZE = 6
MAX_STRONG_SIZE = 32
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
QtGui.QMainWindow.__init__(self, parent)
m_ico = QtGui.QPixmap()
m_ico.loadFromData(base64.b64decode(app_logo))
self.setWindowIcon(Qt.QIcon(m_ico))
self.setWindowTitle('Password Generator')
self.resize(360, 100)
self.window = Qt.QWidget()
self.layout_main = Qt.QGridLayout()
self.layout_main.setContentsMargins(3, 3, 3, 3)
self.layout_main.setSpacing(3)
self.window.setLayout(self.layout_main)
lbl1 = QtGui.QLabel()
m_ico = QtGui.QPixmap()
m_ico.loadFromData(base64.b64decode(pass_easy))
lbl1.setPixmap(QtGui.QPixmap(m_ico))
self.textPasswd1 = Qt.QLineEdit()
self.textPasswd1 = Qt.QLineEdit()
self.textPasswd1Len = Qt.QLineEdit()
self.textPasswd1Len.setText('4')
self.textPasswd1Len.setFixedWidth(25)
self.textPasswd1Len.setValidator(Qt.QIntValidator(2,5))
self.textPasswd1Len.textChanged.connect(self.get_passwd_simple)
self.get_passwd_simple()
self.layout_main.addWidget(lbl1,0,0)
self.layout_main.addWidget(self.textPasswd1,0,1)
self.layout_main.addWidget(self.textPasswd1Len,0,2)
m_ico = QtGui.QPixmap()
m_ico.loadFromData(base64.b64decode(refresh_icon))
new_act1 = QtGui.QPushButton()
new_act1.setIcon(QtGui.QIcon(m_ico))
new_act1.setShortcut('Ctrl+1')
self.layout_main.addWidget(new_act1,0,3)
self.connect(new_act1, QtCore.SIGNAL('clicked()'), self.get_passwd_simple)
lbl2 = QtGui.QLabel()
m_ico = QtGui.QPixmap()
m_ico.loadFromData(base64.b64decode(pass_medium))
lbl2.setPixmap(QtGui.QPixmap(m_ico))
self.textPasswd2 = Qt.QLineEdit()
self.textPasswd2Len = Qt.QLineEdit()
self.textPasswd2Len.setText('12')
self.textPasswd2Len.setFixedWidth(25)
self.textPasswd2Len.setValidator(Qt.QIntValidator(MIN_MEDIUM_SIZE,MAX_MEDIUM_SIZE))
self.textPasswd2Len.textChanged.connect(self.get_passwd_medium)
self.get_passwd_medium()
self.layout_main.addWidget(lbl2,1,0)
self.layout_main.addWidget(self.textPasswd2,1,1)
self.layout_main.addWidget(self.textPasswd2Len,1,2)
m_ico = QtGui.QPixmap()
m_ico.loadFromData(base64.b64decode(refresh_icon))
new_act2 = QtGui.QPushButton()
new_act2.setIcon(QtGui.QIcon(m_ico))
new_act2.setShortcut('Ctrl+2')
self.layout_main.addWidget(new_act2,1,3)
self.connect(new_act2, QtCore.SIGNAL('clicked()'), self.get_passwd_medium)
lbl3 = QtGui.QLabel()
m_ico = QtGui.QPixmap()
m_ico.loadFromData(base64.b64decode(pass_strong))
lbl3.setPixmap(QtGui.QPixmap(m_ico))
self.textPasswd3 = Qt.QLineEdit()
self.textPasswd3Len = Qt.QLineEdit()
self.textPasswd3Len.setText('24')
self.textPasswd3Len.setFixedWidth(25)
self.textPasswd3Len.setValidator(Qt.QIntValidator(MIN_STRONG_SIZE,MAX_STRONG_SIZE))
self.textPasswd3Len.textChanged.connect(self.get_passwd_strong)
self.get_passwd_strong()
self.layout_main.addWidget(lbl3,2,0)
self.layout_main.addWidget(self.textPasswd3,2,1)
self.layout_main.addWidget(self.textPasswd3Len,2,2)
m_ico = QtGui.QPixmap()
m_ico.loadFromData(base64.b64decode(refresh_icon))
new_act3 = QtGui.QPushButton()
new_act3.setIcon(QtGui.QIcon(m_ico))
new_act3.setShortcut('Ctrl+3')
self.layout_main.addWidget(new_act3,2,3)
self.connect(new_act3, QtCore.SIGNAL('clicked()'), self.get_passwd_strong)
m_ico = QtGui.QPixmap()
m_ico.loadFromData(base64.b64decode(exit_icon))
quit_act = QtGui.QPushButton()
quit_act.setIcon(QtGui.QIcon(m_ico))
quit_act.setShortcut('Ctrl+Q')
self.layout_main.addWidget(quit_act,3,0,1,4)
self.connect(quit_act, QtCore.SIGNAL('clicked()'), QtGui.qApp, QtCore.SLOT('quit()'))
self.setCentralWidget(self.window)
def get_passwd_simple(self):
L1 = ['a','e','i','o','u']
L2 = [chr(t) for t in range(ord('a'),ord('z')+1) if chr(t) not in L1]
PASS = ''
T = self.textPasswd1Len.text()
if T: LEN = int(T)
else: return
if LEN < 2: return
for t in xrange(0,LEN):
TWO = two_en[0]
while TWO in two_en:
LIT1 = random.choice(L2)
LIT2 = random.choice(L1)
TWO = LIT1 + LIT2
L1.remove(LIT2)
L2.remove(LIT1)
PASS += TWO
self.textPasswd1.setText(PASS)
def get_passwd_medium(self):
RND1 = [chr(t) for t in range(ord('a'),ord('z')+1) if chr(t) not in 'ilo']
RND2 = [chr(t) for t in range(ord('A'),ord('Z')+1) if chr(t) not in 'ILO']
RND3 = [chr(t) for t in range(ord('2'),ord('9')+1)]
T = self.textPasswd2Len.text()
if T: LEN = int(T)
else: return
if LEN < MIN_MEDIUM_SIZE: return
RND_T = [RND1,RND2,RND3]
IDX = 0
PRE = []
for t in xrange(0,LEN):
PRE += [random.choice(RND_T[IDX])]
IDX += 1
if IDX >= len(RND_T): IDX = 0
REZ = ''
while PRE:
REZ += PRE.pop(random.choice(xrange(0,len(PRE))))
self.textPasswd2.setText(REZ)
def get_passwd_strong(self):
RND1 = [chr(t) for t in range(ord('a'),ord('z')+1) if chr(t) not in 'ilo']
RND2 = [chr(t) for t in range(ord('A'),ord('Z')+1) if chr(t) not in 'ILO']
RND3 = [chr(t) for t in range(ord('2'),ord('9')+1)]
RND4 = list('!@#$%^&*')
T = self.textPasswd3Len.text()
if T: LEN = int(T)
else: return
if LEN < MIN_STRONG_SIZE: return
RND_T = [RND1,RND2,RND3,RND4]
IDX = 0
PRE = []
for t in xrange(0,LEN):
PRE += [random.choice(RND_T[IDX])]
IDX += 1
if IDX >= len(RND_T): IDX = 0
REZ = ''
while PRE:
REZ += PRE.pop(random.choice(xrange(0,len(PRE))))
self.textPasswd3.setText(REZ)
if __name__ == "__main__" :
app = QtGui.QApplication(sys.argv)
main = MainWindow()
main.show()
sys.exit(app.exec_())
# The end is near!

Сохраняем в PassGen.pyw.

Для сборки могут потребоваться ещё такие файлы, которые так же можно найти в архиве в конце статьи:
- png2ico.exe - маленькая утилитка для сборки ico из png файлов.
- msvcp90.dll - библиотека из MS Redist 2010.

Если всё сделали правильно - по двойному клику на build.cmd запустится процесс сборки нашего файла. Результат сборки будет в папке dist. Запускаем exe файл и наслаждаемся результатом:

Простой пароль: от 2 до 5 слогов. Подходит для ручного ввода.
Средний пароль: от 6 до 32 знаков, буквы, БУКВЫ, цифры. Исключены схожие по написанию i, l, o, I, L, O, 1, 0.
Сложный пароль: от 6 до 32 знаков, буквы, БУКВЫ, цифры, символы. Исключены схожие по написанию i, l, o, I, L, O, 1, 0.

Горячие клавиши:
CTRL+1..3 - Новый пароль
CTRL+Q - Выход

Для меня чаще всего нужны именно такие варианты паролей. Надеюсь для Вас данная информация будет полезной.

Ссылки:
Архив со всеми скриптами: PassGenSrc.7z
Готовый exe файл в архиве: PassGen.7z
Есть вопросы? Мои контакты тут
Отдельная благодарность сайту dumpz.org за красивые дампы исходных файлов.