🛠

OpenSiv3D | OpenSiv3D | GUI ウィジェット集

10 min read

OpenSiv3D v0.4.3 標準の SimpleGUI に含まれていない、追加の GUI 機能です。

1. リストボックス

# include <Siv3D.hpp> // OpenSiv3D v0.4.3

namespace s3d::UIToolkit
{
	class ListBox
	{
	private:

		int32 m_frameThickness = 1;

		int32 m_paddingLeft = 8;

		int32 m_paddingRight = 8;

		int32 m_scrollBarWidth = 18;

		Array<String> m_items;

		Rect m_rect = Rect(0);

		Font m_font;

		int32 m_maxLines = 0;

		bool m_hasScrollBar = false;

		int32 m_scroll = 0;

		double m_wheel = 0.0;

		Optional<size_t> m_selectedItemIndex;

		Optional<std::pair<int32, double>> m_scrollBarGrabbed;

	public:

		ListBox() = default;

		ListBox(const Array<String>& items, const Rect& rect, const Font& font, const Optional<size_t>& selectedItemIndex = none)
			: ListBox(items, rect, 8, 8, 18, font, selectedItemIndex) {}

		ListBox(const Array<String>& items, const Rect& rect, int32 paddingLeft, int32 paddingRight, int32 scrollBarWidth,
			const Font& font, const Optional<size_t>& selectedItemIndex = none)
			: m_paddingLeft(paddingLeft)
			, m_paddingRight(paddingRight)
			, m_scrollBarWidth(scrollBarWidth)
			, m_items(items)
			, m_rect(rect.pos, Max(rect.w, 40), Max(rect.h, font.height() + (m_frameThickness * 2)))
			, m_font(font)
			, m_maxLines((m_rect.h - (m_frameThickness * 2)) / font.height())
			, m_hasScrollBar(m_maxLines < m_items.size())
			, m_selectedItemIndex(selectedItemIndex) {}

		void update()
		{
			const int32 fontHeight = m_font.height();
			const int32 lines = Min(static_cast<int32>(m_items.size()), m_maxLines);
			const int32 itemWidth = (m_rect.w - (m_frameThickness * 2) - (m_hasScrollBar ? m_scrollBarWidth : 0));

			for (int32 i = 0; i < lines; ++i)
			{
				const size_t itemIndex = (m_scroll + i);
				const Rect itemRect(m_rect.x + m_frameThickness, m_rect.y + m_frameThickness + (fontHeight * i), itemWidth, fontHeight);

				if (itemRect.leftClicked())
				{
					m_selectedItemIndex = itemIndex;
				}
			}

			if (!m_hasScrollBar)
			{
				return;
			}

			const Rect scrollBarArea(m_rect.x + m_rect.w - (m_frameThickness + m_scrollBarWidth), m_rect.y + m_frameThickness, m_scrollBarWidth, m_rect.h - (m_frameThickness * 2));
			const Rect upButton(scrollBarArea.pos, m_scrollBarWidth, m_scrollBarWidth);
			const Rect downButton(scrollBarArea.x, scrollBarArea.y + scrollBarArea.h - m_scrollBarWidth, m_scrollBarWidth, m_scrollBarWidth);

			if (upButton.leftClicked())
			{
				m_scroll = Max(m_scroll - 1, 0);
			}

			if (downButton.leftClicked())
			{
				m_scroll = Min(m_scroll + 1, static_cast<int32>(m_items.size()) - m_maxLines);
			}

			const Rect innerScrollBarArea = scrollBarArea.stretched(0, -m_scrollBarWidth);
			const int32 scrollBarHeight = static_cast<int32>(innerScrollBarArea.h * (static_cast<double>(m_maxLines) / m_items.size()));
			const int32 scrollBarOffset = static_cast<int32>((innerScrollBarArea.h - scrollBarHeight) * (static_cast<double>(m_scroll) / (m_items.size() - m_maxLines)));
			const Rect scrollBar(innerScrollBarArea.pos.movedBy(0, scrollBarOffset), innerScrollBarArea.w, scrollBarHeight);

			if (m_scrollBarGrabbed && MouseL.up())
			{
				m_scrollBarGrabbed.reset();
			}

			if ((m_rect.mouseOver() && Mouse::Wheel()) || m_scrollBarGrabbed)
			{
				if (m_rect.mouseOver() && Mouse::Wheel())
				{
					m_wheel += Mouse::Wheel();
				}

				if (m_scrollBarGrabbed)
				{
					const double ih = static_cast<double>(innerScrollBarArea.h) / m_items.size();
					m_wheel = m_scrollBarGrabbed->second + ((Cursor::Pos().y - m_scrollBarGrabbed->first) / ih);
				}

				const double maxWheel = static_cast<int32>(m_items.size()) - m_maxLines;
				m_wheel = Clamp(m_wheel, 0.0, maxWheel);
				m_scroll = static_cast<int32>(m_wheel);
			}
			else
			{
				m_wheel = m_scroll;
			}

			if (scrollBar.leftClicked())
			{
				m_scrollBarGrabbed = std::pair{ Cursor::Pos().y, m_wheel };
			}
		}

		void draw() const
		{
			m_rect
				.draw()
				.drawFrame(1, 0, ColorF(0.5));

			const int32 fontHeight = m_font.height();
			const int32 lines = Min(static_cast<int32>(m_items.size()), m_maxLines);
			const int32 itemWidth = (m_rect.w - (m_frameThickness * 2) - (m_hasScrollBar ? m_scrollBarWidth : 0));
			const int32 textMaxWidth = itemWidth - (m_paddingLeft + m_paddingRight);

			for (int32 i = 0; i < lines; ++i)
			{
				const size_t itemIndex = (m_scroll + i);
				const bool selected = (itemIndex == m_selectedItemIndex);
				const Rect itemRect(m_rect.x + m_frameThickness, m_rect.y + m_frameThickness + (fontHeight * i), itemWidth, fontHeight);
				const Rect textRect(itemRect.pos.movedBy(m_paddingLeft, 0), textMaxWidth, itemRect.h);
				const ColorF textColor = selected ? ColorF(1.0) : ColorF(0.1);
				constexpr ColorF selectedItemBackgroundColor(0.2, 0.4, 0.8);
				const String& text = m_items[itemIndex];

				if (selected)
				{
					itemRect
						.stretched(-1, 0, 0, -1)
						.draw(selectedItemBackgroundColor);
				}

				m_font(text).draw(textRect, textColor);
			}

			if (!m_hasScrollBar)
			{
				return;
			}

			const Rect scrollBarArea(m_rect.x + m_rect.w - (m_frameThickness + m_scrollBarWidth), m_rect.y + m_frameThickness, m_scrollBarWidth, m_rect.h - (m_frameThickness * 2));
			const Rect upButton(scrollBarArea.pos, m_scrollBarWidth, m_scrollBarWidth);
			const Rect downButton(scrollBarArea.x, scrollBarArea.y + scrollBarArea.h - m_scrollBarWidth, m_scrollBarWidth, m_scrollBarWidth);

			scrollBarArea.draw(ColorF(0.85));

			{
				const bool pressed = !m_scrollBarGrabbed && upButton.leftPressed();
				const Vec2 c = upButton.center();

				if (pressed)
				{
					upButton
						.stretched(-1)
						.draw(ColorF(0.33));
				}

				Triangle(c.movedBy(0, -m_scrollBarWidth * 0.15),
					c.movedBy(m_scrollBarWidth * 0.25, m_scrollBarWidth * 0.15),
					c.movedBy(-m_scrollBarWidth * 0.25, m_scrollBarWidth * 0.15))
					.draw(pressed ? ColorF(0.85) : ColorF(0.33));
			}

			{
				const bool pressed = !m_scrollBarGrabbed && downButton.leftPressed();
				const Vec2 c = downButton.center();

				if (pressed)
				{
					downButton
						.stretched(-1)
						.draw(ColorF(0.33));
				}

				Triangle(c.movedBy(0, m_scrollBarWidth * 0.15),
					c.movedBy(-m_scrollBarWidth * 0.25, -m_scrollBarWidth * 0.15),
					c.movedBy(m_scrollBarWidth * 0.25, -m_scrollBarWidth * 0.15))
					.draw(pressed ? ColorF(0.85) : ColorF(0.33));
			}

			const Rect innerScrollBarArea = scrollBarArea.stretched(0, -m_scrollBarWidth);
			const int32 scrollBarHeight = static_cast<int32>(innerScrollBarArea.h * (static_cast<double>(m_maxLines) / m_items.size()));
			const int32 scrollBarOffset = static_cast<int32>((innerScrollBarArea.h - scrollBarHeight) * (static_cast<double>(m_scroll) / (m_items.size() - m_maxLines)));
			const Rect scrollBar(innerScrollBarArea.pos.movedBy(0, scrollBarOffset), innerScrollBarArea.w, scrollBarHeight);

			scrollBar
				.stretched(-1, 0)
				.draw(m_scrollBarGrabbed ? ColorF(0.33) : ColorF(0.67));
		}

		[[nodiscard]]
		const Array<String>& getItems() const noexcept
		{
			return m_items;
		}

		void setPos(const Point& pos)
		{
			m_rect.setPos(pos);
		}

		[[nodiscard]]
		const Rect& getRect() const noexcept
		{
			return m_rect;
		}

		void setIndex(const Optional<size_t>& index)
		{
			assert((index == none) || (index.value() < m_items.size()));
			m_selectedItemIndex = index;
		}

		[[nodiscard]]
		const Optional<size_t> getIndex() const noexcept
		{
			return m_selectedItemIndex;
		}
	};
}

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF(0.8, 0.9, 1.0));

	const Array<String> items1 = {
		U"北海道", U"青森県", U"岩手県", U"宮城県", U"秋田県", U"山形県", U"福島県", U"茨城県",
		U"栃木県", U"群馬県", U"埼玉県", U"千葉県", U"東京都", U"神奈川県", U"新潟県", U"富山県",
		U"石川県", U"福井県", U"山梨県", U"長野県", U"岐阜県", U"静岡県", U"愛知県", U"三重県",
		U"滋賀県", U"京都府", U"大阪府", U"兵庫県", U"奈良県", U"和歌山県", U"鳥取県", U"島根県",
		U"岡山県", U"広島県", U"山口県", U"徳島県", U"香川県", U"愛媛県", U"高知県", U"福岡県",
		U"佐賀県", U"長崎県", U"熊本県", U"大分県", U"宮崎県", U"鹿児島県", U"沖縄県",
	};
	const Array<String> items2 = {
		U"吾輩は猫である(1905年1月 - 1906年8月、『ホトトギス』/1905年10月 - 1907年5月、大倉書店・服部書店)",
		U"坊っちゃん(1906年4月、『ホトトギス』/1907年、春陽堂刊『鶉籠』収録)",
		U"草枕(1906年9月、『新小説』/『鶉籠』収録)",
		U"二百十日(1906年10月、『中央公論』/『鶉籠』収録)",
		U"野分(1907年1月、『ホトトギス』/1908年、春陽堂刊『草合』収録)",
		U"虞美人草(1907年6月 - 10月、『朝日新聞』/1908年1月、春陽堂)",
		U"坑夫(1908年1月 - 4月、『朝日新聞』/『草合』収録)",
		U"三四郎(1908年9 - 12月、『朝日新聞』/1909年5月、春陽堂)",
		U"それから(1909年6 - 10月、『朝日新聞』/1910年1月、春陽堂)",
		U"門(1910年3月 - 6月、『朝日新聞』/1911年1月、春陽堂)",
		U"彼岸過迄(1912年1月 - 4月、『朝日新聞』/1912年9月、春陽堂)",
		U"行人(1912年12月 - 1913年11月、『朝日新聞』/1914年1月、大倉書店)",
		U"こゝろ(1914年4月 - 8月、『朝日新聞』/1914年9月、岩波書店)",
		U"道草(1915年6月 - 9月、『朝日新聞』/1915年10月、岩波書店)",
		U"明暗(1916年5月 - 12月、『朝日新聞』/1917年1月、岩波書店)",
	};

	const Font font1(16), font2(12);
	Array<UIToolkit::ListBox> listboxes = {
		UIToolkit::ListBox(items1, Rect(120, 100, 120, 200), font1),
		UIToolkit::ListBox(items1, Rect(300, 100, 80, 90), 4, 4, 12, font2),
		UIToolkit::ListBox(items2, Rect(440, 100, 400, 400), font1),
	};

	while (System::Update())
	{
		ClearPrint();

		for (auto& listbox : listboxes)
		{
			listbox.update();
		}

		for (const auto& listbox : listboxes)
		{
			listbox.draw();

			if (const auto index = listbox.getIndex())
			{
				Print << listbox.getItems()[index.value()];
			}
		}
	}
}

Discussion

ログインするとコメントできます