2026, Jan 02 09:02
Как удержать многоформатный буфер обмена X11 с Qt и xclip
Почему в X11 «копия» исчезает после закрытия приложения, как работать с множеством MIME-типов, удерживать владение буфером и использовать Qt/менеджер буфера.
Копирование изображения в буфер обмена X11 сразу в нескольких форматах — например, image/png и image/jpeg — кажется простой задачей, пока не пытаешься сохранить содержимое после завершения программы, которая его установила. Утилиты вроде xclip позволяют отправить данные в буфер, но только с одним целевым типом за раз; следующий вызов перетирает предыдущий. Фреймворки вроде Qt умеют одновременно объявлять множество форматов, однако, как только приложение закрывается, буфер возвращается к тому, что осталось доступным. Ниже — почему так происходит, что это означает для ваших инструментов и как добиться нужного поведения.
Демонстрация проблемы
Для начала проверим очевидный путь с xclip: видно, что «последняя запись побеждает», ранние цели теряются.
xclip -selection clipboard -t image/png -i image.png
xclip -selection clipboard -t image/jpg -i image.jpg
xclip -selection clipboard -t TARGETS -o
TARGETS
image/jpgС Qt нужное поведение с несколькими целями работает, пока приложение активно. Пример ниже загружает изображение, публикует его и как «родную» картинку, и как явные цели image/png и image/jpeg, а затем завершает работу по нажатию Enter. Имена переменных могут отличаться от других примеров, но логика та же.
import base64
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QImage, QClipboard
from PyQt5.QtCore import QMimeData, QByteArray, QBuffer, QIODevice
# Загружаем файл, переводим в base64-текст и возвращаем к байтам
with open("image.png", "rb") as fh:
b64_blob = base64.b64encode(fh.read()).decode()
img_raw = base64.b64decode(b64_blob)
# Создаём QImage из байтового потока
pic = QImage.fromData(img_raw)
if pic.isNull():
raise ValueError("Invalid image")
# Запускаем стек Qt
gui = QApplication([])
# Готовим содержимое для буфера обмена
payload = QMimeData()
# Публикуем «родное» изображение
payload.setImageData(pic)
# Дополнительно явно выставляем image/png
bin_png = QByteArray()
io_slot = QBuffer(bin_png)
io_slot.open(QIODevice.WriteOnly)
pic.save(io_slot, "PNG")
io_slot.close()
payload.setData("image/png", bin_png)
# И явно выставляем image/jpeg
bin_jpg = QByteArray()
io_slot = QBuffer(bin_jpg)
io_slot.open(QIODevice.WriteOnly)
pic.save(io_slot, "JPEG")
io_slot.close()
payload.setData("image/jpeg", bin_jpg)
# Отправляем в буфер обмена
QApplication.clipboard().setMimeData(payload)
print("Image placed on clipboard in multiple formats.")
# Оставляем приложение жить, чтобы буфер оставался доступен
gui.processEvents()
input("Press Enter to quit...")Если сейчас посмотреть цели буфера обмена, будет длинный список форматов изображений, которые объявляет Qt. Но как только приложение закрывается, содержимое буфера меняется.
Что на самом деле происходит и почему
В X11 буфер обмена — это не центральный буфер. Приложение, выполняющее «копирование», становится владельцем выделения. Когда другое приложение просит вставку или запрашивает конкретную цель, оно обращается напрямую к текущему владельцу. Отсюда следствие: два отдельных источника не могут одновременно предоставлять разные цели; тот, кто последним заявил права на буфер, становится единственным владельцем, а предыдущий эти права теряет. Поэтому последний вызов xclip и «побеждает», стирая всё, что было до него.
Qt ведёт себя иначе, потому что объявляет множество целей сразу, оставаясь единым владельцем. Пока ваше приложение на Qt запущено, оно может предлагать широкий набор форматов и по требованию конвертировать данные под запрошенный MIME-тип. Как только приложение завершается, оно перестаёт обслуживать выделение, и буфер переходит к тому, кто заявит его следующим. В X11 нет «режима постоянства», который переживал бы выход исходного поставщика.
Видимая «устойчивость» буфера в настольных окружениях чаще всего обеспечивается менеджером буфера обмена: он слушает изменения, забирает данные у текущего владельца и затем сам становится владельцем, продолжая раздавать то же содержимое после выхода исходной программы. Другой частый сценарий — источник просто никуда не закрывался. Даже xclip демонстрирует это поведение, оставаясь в фоне и удерживая владение, пока его не перехватят:
echo "Hello" | xclip -in -sel clipboard
pgrep -alf xclipРешение: держать поставщика живым или передать данные менеджеру
Чтобы многопрофильные данные изображения оставались доступны, источник должен продолжать жить как владелец выделения — либо менеджер буфера должен заранее забрать и заново раздавать содержимое. Для приложения на Qt можно просто не завершаться и выйти лишь тогда, когда владение утрачено. И нет необходимости вручную выставлять каждый MIME-тип: достаточно поместить изображение, Qt при необходимости сконвертирует его в разные форматы.
Ниже пример: он помещает QImage в буфер и не закрывается. Программа подписывается на сигнал dataChanged и проверяет ownsClipboard; как только владение потеряно, приложение завершается. Инструмент сосредоточен на роли поставщика буфера, а Qt сам объявляет множество целей изображения без явного заполнения каждого MIME-типа.
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QImage
from PyQt5.QtCore import QTimer
# Загружаем изображение и публикуем только как «родное»
with open("image.png", "rb") as f:
raw_bytes = f.read()
img_obj = QImage.fromData(raw_bytes)
if img_obj.isNull():
raise ValueError("Invalid image")
app_ctx = QApplication([])
cb = app_ctx.clipboard()
cb.setImage(img_obj)
# Завершаем работу, когда перестаём владеть буфером
def stop_when_lost():
if not cb.ownsClipboard():
app_ctx.quit()
cb.dataChanged.connect(stop_when_lost)
# Запускаем цикл событий и обслуживаем буфер, пока не изменится владелец
QTimer.singleShot(0, lambda: None)
app_ctx.exec_()Почему это важно
Инструменты, работающие с буфером обмена X11, нужно проектировать с учётом его модели владения. Если предположить существование центрального буфера, поведение получится хрупким: несколько вызовов xclip будут мешать друг другу, а помощник, который сразу завершается, не сможет удержать содержимое. Понимание того, что поставщик должен оставаться активным, объясняет, почему браузеры и файловые менеджеры будто бы предоставляют «постоянный» насыщенный буфер, и почему минимальная утилита должна либо продолжать работу, либо полагаться на менеджер буфера, который зеркалирует данные.
Выводы
Относитесь к буферу обмена X11 как к «живому контракту» между одним производителем и множеством потребителей. Используйте одну программу, которая объявляет все форматы сразу, вместо цепочки отдельных записей. Если вы применяете Qt, достаточно установить «родное» изображение — Qt предложит широкий набор MIME-целей и выполнит конвертацию по запросу. Держите процесс запущенным и завершайте его, когда потеряете владение, или делегируйте постоянство менеджеру буфера, если он уже работает в системе.