LoginSignup
5
3

More than 5 years have passed since last update.

Python+Qt(PySide)で色相環のカラーピッカーを作成

Last updated at Posted at 2017-02-07

 ありそうで意外と無かったので、色相環のカラーピッカーのWidgetを作成しました。
 当初、C言語のノリで低レベルの処理を記述したところ、高解像度モニタ用の動作確認で拡大表示するとスローモーションになってしまい、QtのライブラリやPython流の最適化をしたら、問題なく動作するようになりました。
Pythonを使うなら、Pythonの流儀を覚えろってことですね。

 中央の彩度、明度のカラーボックスのグラデーションは、彩度と透明度の2つのグラデーションを合成して実現しています。海外の掲示板で紹介されていた方法です。

 PyPIに登録しました。pip等でinstallできます。
 ソースをGitHubに登録しました。
https://github.com/tokyo-noctambulist/colorpicker.git

(2017/03/07 更新)
  α値を破壊していたバグを修正。
  色見本、色相環にα値を反映してBGをタイル塗りするように変更
  色指定時に、値を参照渡しをcopyに変更

サンプル

image

 sample.pyでは、2つの色相環を連動させて表示しています。


簡易説明

 PyPIに登録しました。pip等でinstallできます。

function
pip install colorpicker #install

from colorpicker import *

colorCircle = QHueCircleWidget(self)
color = colorCircle.getColor() #色を収得
colorCircle.setColor(QColor(255,100,10,200)) #色をセット

colorCircle.colorChanged.connect(onUpdateColor) #signal-slot

resize()で、サイズを変更できます。
ただし、縦横比を1対1にしないと正常に動作しません。(楕円状態に未対応 orz)


QHueCircleWidget.py
# coding:utf-8
from __future__ import division, print_function, unicode_literals, absolute_import
# noinspection PyUnresolvedReferences
from future_builtins import *

from PySide.QtGui import *
from PySide.QtCore import *

import math

class QHueCircleWidget(QGraphicsView):
    PRESS_NONE = 0
    PRESS_BOX = 1
    PRESS_CIRCLE = 2

    colorChanged = Signal(str)

    def __init__(self, parent):
        super(QHueCircleWidget, self).__init__(parent)
        self.scene = QGraphicsScene(self)
        self.setScene(self.scene)
        self.rectF = None
        self.press_mode = self.PRESS_NONE
        self.color = QColor()
        self.color.setHsv(0, 255, 255, 255)
        self.setColor(self.color)
        self.bg_brush = QBrush(self.create_bg_pixmap())

        # self.setFixedSize(200,200)
        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
        self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
        self.setMinimumSize(200, 200)


    def drawBackground(self, painter, rect):
        """
        :type rect: QRectF
        :type painter: QPainter
        """
        painter.save()

        painter.translate(rect.center())
        painter.setRenderHint(QPainter.Antialiasing, True)
        self.rectF = rect
        self.rectToParam()
        painter.setPen(QPen(Qt.black, 1))
        # draw select color
        painter.setBrush(self.bg_brush)
        painter.drawEllipse(-self.half_width, -self.half_height, self.width / 8, self.height / 8)
        painter.setBrush(self.color)
        painter.drawEllipse(-self.half_width, -self.half_height, self.width / 8, self.height / 8)
        # hue color circle
        painter.setBrush(self.bg_brush)
        painter.drawEllipse(0 - self.half_width, 0 - self.half_height, self.width, self.height)
        color = QColor()
        gradient = QConicalGradient(0, 0, 0)
        for _ in range(11):
            color.setHsv((10 - _) * 36, 255, 255, self.color.alpha())
            gradient.setColorAt(0.1 * _, color)
        painter.setBrush(gradient)
        painter.drawEllipse(0 - self.half_width, 0 - self.half_height, self.width, self.height)
        # center delete circle
        mini_circle_width = self.width * 3 / 4
        mini_circle_height = self.height * 3 / 4
        painter.setBrush(QPalette().brush(QPalette.Midlight))
        painter.drawEllipse(0 - mini_circle_width / 2, 0 - mini_circle_height / 2 , mini_circle_width, mini_circle_height)
        # center box saturation
        gradient = QLinearGradient(-self.quarter_width, 0, self.quarter_width, 0)
        color.setHsv(self.color.hue(), 0, 255, 255)
        gradient.setColorAt(0.0, color)
        color.setHsv(self.color.hue(), 255, 255, 255)
        gradient.setColorAt(1.0, color)
        painter.setBrush(gradient)
        painter.drawRect(-self.quarter_width, -self.quarter_height, self.half_width, self.half_height)

        painter.restore()

    def drawForeground(self, painter, rect):
        """
        :type rect: QRectF
        :type painter: QPainter
        """
        painter.save()

        painter.translate(rect.center())
        painter.setRenderHint(QPainter.Antialiasing, True)
        # center box value
        gradient = QLinearGradient(0, -self.quarter_height, 0, self.quarter_height)
        color = QColor()
        color.setHsv(0, 0, 0, 0)
        gradient.setColorAt(0.0, color)
        color.setHsv(0, 0, 0, 255)
        gradient.setColorAt(1.0, color)
        painter.setBrush(gradient)
        painter.drawRect(-self.quarter_width, -self.quarter_height, self.half_width, self.half_height)
        # lupe for Saturation / Value
        painter.setPen(QPen(QColor(255, 255, 255, 255), 1))
        painter.drawEllipse(self.sv_x - 3, self.sv_y - 3, 7, 7)
        painter.setPen(QPen(QColor(0, 0, 0, 255), 1))
        painter.drawEllipse(self.sv_x - 2, self.sv_y - 2, 5, 5)
        # lupe for Hue
        painter.setPen(QPen(QColor(0, 0, 0, 255), 1))
        painter.setBrush(QColor(255, 255, 255, 0))
        painter.rotate(self.color.hue())
        painter.drawRect(self.width * 3 / 8, -4, self.width / 8, 8)

        painter.restore()

    def mouseMoveEvent(self, event):
        self.clickToColor(event)

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.rectToParam()
            if self.hitBox(event.x(), event.y()):
                self.press_mode = self.PRESS_BOX
                self.clickToColor(event)
            elif self.hitCircle(event.x(), event.y()):
                self.press_mode = self.PRESS_CIRCLE
                self.clickToColor(event)
            else:
                self.press_mode = self.PRESS_NONE

    def clickToColor(self, event):
        if self.press_mode == self.PRESS_BOX:
            self.clickToBox(event)
        elif self.press_mode == self.PRESS_CIRCLE:
            self.clickCircle(event)

        self.colorChanged.emit('colorChanged')
        self.scene.update()

    def hitBox(self, x, y):
        rect = QRect(self.quarter_width, self.quarter_height, self.width * 2 / 4, self.height * 2 / 4)
        return rect.contains(x, y)

    def hitCircle(self, x, y):
        path = QPainterPath()
        path.addEllipse(0, 0, self.width, self.height)

        path2 = QPainterPath()
        path2.addEllipse(self.quarter_width / 2., self.quarter_height / 2., self.width / 4. * 3., self.height / 4. * 3.)

        pos = QPointF(x, y)
        return path.contains(pos) and not path2.contains(pos)

    def clickToBox(self, event):
        if not self.hitBox(event.x(), event.y()):
            return

        x = event.x()
        y = event.y()

        col_sat = 255 * (x - self.quarter_width) / self.half_width
        col_sat = 0 if col_sat < 0 else col_sat
        col_sat = 255 if col_sat > 255 else col_sat

        col_val = 255 * (self.half_height - (y - self.quarter_height)) / self.half_height
        col_val = 0 if col_val < 0 else col_val
        col_val = 255 if col_val > 255 else col_val

        self.color.setHsv(
            self.color.hue(),
            col_sat,
            col_val,
            self.color.alpha()
        )
        self.sv_x = x - self.half_width
        self.sv_y = y - self.half_height

    def clickCircle(self, event):
        x = event.x()
        y = event.y()

        k = int(math.degrees(math.atan2(x - self.half_width, self.half_height - y)))
        self.color.setHsv(
            (k - 90) % 360,
            self.color.saturation(),
            self.color.value(),
            self.color.alpha()
        )
        self.hue_x = x - self.half_width
        self.hue_y = y - self.half_height

    def getColor(self):
        return(self.color)

    def setColor(self, color):
        self.color = QColor(color)
        if self.rectF == None:
            self.sv_x = 0
            self.sv_y = 0
            self.hue_x = 0
            self.hue_y = 0
            self.scene.update()
        else:
            self.rectToParam()
            # SV
            self.sv_x = color.saturation() * self.half_width / 255 - self.quarter_width
            self.sv_y = (255 - color.value()) * self.half_height / 255 - self.quarter_height
            # Hue
            radian = ((color.hue()) % 360) * math.pi / 180
            self.hue_x = math.cos(radian) * self.half_width
            self.hue_y = math.sin(radian) * self.half_height
        self.scene.update()

    def rectToParam(self):
        if self.rectF == None:
            rect = QRectF(0, 0, 200, 200)
        else:
            rect = self.rectF

        self.width = rect.toRect().width()
        self.half_width = self.width / 2
        self.quarter_width = self.width / 4

        self.height = rect.toRect().height()
        self.half_height = self.height / 2
        self.quarter_height = self.height / 4

    @staticmethod
    def create_bg_pixmap(color1=None, color2=None):
        """
        :rtype: QPixmap
        """
        pixmap = QPixmap(QSize(16, 16))
        color1 = color1 or QColor(128, 128, 128)
        color2 = color2 or QColor(168, 168, 168)

        painter = QPainter(pixmap)
        painter.save()
        brush1 = QBrush(color1)
        brush2 = QBrush(color2)
        painter.fillRect(0, 0, 8, 8, brush1)
        painter.fillRect(8, 8, 8, 8, brush1)
        painter.fillRect(8, 0, 8, 8, brush2)
        painter.fillRect(0, 8, 8, 8, brush2)
        painter.restore()
        painter.end()

        return pixmap
sample.py
from __future__ import division, print_function, unicode_literals, absolute_import

from PySide.QtGui import *
from PySide.QtCore import *
import sys

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.resize(700, 500)

        self.colorCircle1 = QHueCircleWidget(self)

        self.colorCircle2 = QHueCircleWidget(self)
        self.colorCircle2.resize(400, 400)
        self.colorCircle2.move(260, 50)

        # alpha color sample
        color = QColor(255, 0, 0, 100)
        self.colorCircle1.setColor(color)
        self.colorCircle2.setColor(color)

        self.colorCircle1.colorChanged.connect(self.onUpdateColor1)
        self.colorCircle2.colorChanged.connect(self.onUpdateColor2)

    @Slot()
    def onUpdateColor1(self):
        color = self.colorCircle1.getColor()
        self.colorCircle2.setColor(color)

    @Slot()
    def onUpdateColor2(self):
        color = self.colorCircle2.getColor()
        self.colorCircle1.setColor(color)

def main():
    app = QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3